Contents

Chapter 1

Feast Setup Guide

This guide takes you from an empty machine to a working feature store serving

the AcmeCorp retail-risk features shipped in this package. It assumes Python

3.9+ and basic comfort with the command line.

1. Install

Feast is modular: you install the framework plus the extras for the stores you

use. For the default config in feature_repo/feature_store.yaml (Redis online,

file offline) you need:

bash
python -m venv .venv && source .venv/bin/activate
pip install "feast[redis]" pandas pyarrow

For a zero-infrastructure local run, you can skip Redis entirely and use the

SQLite online store (see step 4). Other common extras: feast[aws],

feast[gcp], feast[snowflake], feast[postgres].

2. Understand the layout

feature-store-bootstrap/
  src/feature_store/        importable library (definitions + helpers)
    definitions.py          entities, sources, batch feature views, services
    transformations.py      pure feature logic + on-demand feature views
    ingestion.py            materialize / push helpers
    retrieval.py            training + serving retrieval helpers
    registry.py             apply / describe / teardown helpers
  feature_repo/
    feature_store.yaml      the project config Feast reads
    example_repo.py         the file `feast apply` scans (imports the library)
    data/                   parquet fixtures + registry.db (created on first run)

The split between src/feature_store/ (a tested library) and feature_repo/

(a thin Feast entry point) is deliberate. Feature logic lives in version-

controlled, unit-tested Python; the repo file just imports and re-exports it so

Feast can discover the objects. As your feature set grows past a single file,

this scales far better than one monolithic example_repo.py.

3. The configuration file, line by line

feature_store.yaml has five things Feast must know:

KeyWhat it controlsProduction swap
projectNamespace for the registry + online keysKeep stable; renaming orphans data
registryWhere the object catalogue livessql registry on Postgres for teams
providerDefault stores + materialization engineaws / gcp for managed infra
online_storeLow-latency serving backendRedis / DynamoDB / Bigtable
offline_storeHistorical retrieval backendBigQuery / Snowflake / Redshift

entity_key_serialization_version: 2 should always be set on new projects --

it is a more compact online-store key encoding. Do not change it on an existing

project, as it would invalidate already-materialized keys.

4. Local run without Redis

Edit feature_repo/feature_store.yaml, comment the redis block, and uncomment

the SQLite alternative:

yaml
online_store:
  type: sqlite
  path: data/online_store.db

SQLite is single-process and slow under load -- perfect for development and CI,

never for production serving.

5. Apply the registry

apply is idempotent: it diffs your definitions against the registry and

creates/updates/deletes objects to match. Run it from the repo directory:

bash
cd feature_repo
feast apply

You should see Feast create the entities, feature views, on-demand view, and the

two feature services. Equivalently, from Python:

python
from feast import FeatureStore
from feature_store.registry import RegistryManager

store = FeatureStore(repo_path="feature_repo")
RegistryManager(store).apply()        # registers everything
RegistryManager(store).describe()     # prints what is now in the registry

6. Materialize features into the online store

Materialization copies feature values from the offline store into the online

store so they can be served in milliseconds.

bash
# One-off backfill of an explicit window:
feast materialize 2024-01-01T00:00:00 2024-01-08T00:00:00

# Scheduled top-up since the last run (the cron-friendly command):
feast materialize-incremental $(date -u +"%Y-%m-%dT%H:%M:%S")

In Python, prefer the helpers in ingestion.py -- they add logging and return

an IngestionStats you can emit to your metrics system:

python
from feature_store.ingestion import materialize_window, materialize_incremental

materialize_window(store, days=7)          # explicit backfill
materialize_incremental(store)             # scheduled top-up

7. Keep the online store fresh between runs

Scheduled materialization has a built-in lag equal to your cron interval. For

features that must be near-real-time (transaction velocity, for example), stream

updates through the PushSource:

python
from feature_store.ingestion import push_micro_batch

push_micro_batch(store, "customer_txn_push_source", fresh_df, mode="online")

mode="online_and_offline" also appends to the offline store so the pushed

value is available for future training joins -- use it when the streamed

aggregate should become part of the historical record.

8. Schedule it in production

A minimal production cadence:

  • Nightly: feast materialize-incremental for slowly-changing views

(profiles, merchant stats) via Airflow / Cron / GitHub Actions.

  • Continuous: a stream consumer calling push_micro_batch for fast-moving

aggregates.

  • On every merge to main: feast apply in CI so registry changes ship with

code. Pin the Feast version in requirements.txt so apply behaviour is

reproducible.

9. Troubleshooting

SymptomLikely causeFix
feast apply finds no objectsRepo file did not import the definitionsConfirm example_repo.py re-exports them and ../src is importable
Online lookup returns all NoneFeatures never materialized, or entity key absentRun materialize-incremental; verify the customer_id exists
Training values look "from the future"Entity timestamp earlier than feature event timeSet the entity event_timestamp to the label time, not now
ConnectionError to RedisOnline store unreachableStart Redis or switch to the SQLite online store
Stale features in a long-running serverRegistry cachedSet registry.cache_ttl_seconds or call RegistryManager.refresh()

With apply, materialize, and serve working, move on to

feature-engineering-patterns.md for how to design features that stay correct

in production.

Chapter 2

Feature Engineering Patterns

Practical patterns for building features that stay correct once they hit

production. Every example references the real objects in this package

(customer_txn_stats, transaction_risk_features, the fraud_detection_v1

service) so you can map the idea straight onto code.

Point-in-time correctness (the whole reason a feature store exists)

The defining job of the offline store is to answer: *"what did we know about

this entity at this exact moment?"* If your training set is built with a naive

JOIN on the latest feature row, you leak future information into the past and

your offline metrics become a fantasy.

Feast enforces this with the entity dataframe's timestamp column. For each

training row it finds the most recent feature value whose event_timestamp is

at or before the row's timestamp, and never after:

python
entity_df = build_entity_dataframe(
    entity_rows=[{"customer_id": 1001, "churned": 0}],
    event_timestamps=[label_observed_at],   # the label's event time, NOT now
)
get_training_dataframe(store, entity_df, features=churn_model_v1)

Rule of thumb: the entity timestamp is the moment the *label* became true,

not the moment you run the training job.

Train/serve skew: one definition, two paths

Skew is when the feature a model trains on differs from the feature it serves

on -- the quietest, most expensive bug in applied ML. The cure is to compute a

feature exactly once and read it through both paths.

This package does that by binding a model to a FeatureService:

python
# Training and serving both reference the SAME service object.
get_training_dataframe(store, entity_df, features=fraud_detection_v1)   # offline
ServingClient(store, "fraud_detection_v1").get_features(entity_row, ctx)  # online

Because both read fraud_detection_v1, adding or removing a feature changes

both paths together. Never hand a model an ad-hoc feature list at serving time

that differs from what it trained on.

On-demand vs. precomputed features

Decide where a feature is computed by how its inputs arrive:

Input availabilityCompute locationExample in this package
Known ahead of time, per entityBatch feature view (materialized)txn_amount_avg_30d
Only known at request timeOn-demand feature viewamount_to_avg_ratio
BothOn-demand view combining themtransaction_risk_features

amount_to_avg_ratio *cannot* be precomputed -- it depends on the live

transaction amount, which does not exist until the request. So it lives in an

on-demand view that combines the request (transaction_request) with the

materialized average. Anything that depends only on the entity (the 30-day

average itself) should be precomputed once, not recomputed on every request.

Keep the maths out of the wrapper (testability)

On-demand views are hard to test if the logic lives inside the decorated

function. This package factors every calculation into a pure function:

python
# transformations.py -- pure, unit-tested
def zscore(value, mean, std): ...

# the on-demand view is a thin adapter over the pure function
@on_demand_feature_view(...)
def transaction_risk_features(inputs):
    out["amount_zscore"] = zscore(inputs["transaction_amount"], ...)

Now zscore has real test coverage (tests/test_definitions.py) independent of

Feast, pandas plumbing, or a running store. Apply the same split to your own

features.

Windowed aggregations

Most behavioural features are windowed counts/sums/averages (txn_count_7d,

txn_amount_avg_30d). Two things keep them honest:

1. Name the window in the feature. txn_count_7d is unambiguous;

txn_count invites a future engineer to change the window and silently

shift the model's inputs.

2. Compute them in the warehouse, not at request time. Sliding-window

aggregates over months of events are expensive; precompute them on a

schedule and materialize. The on-demand layer is for cheap, request-coupled

maths only.

Defensive transforms: zeros, nulls, and cold starts

Every transform runs on rows you did not anticipate -- a customer with no

history, a zero average, a NaN standard deviation. Bake the defence into the

pure function:

python
def ratio_to_average(value, average):
    safe_avg = average.where(average > 0, other=1.0)   # no divide-by-zero
    ratio = value / safe_avg
    return ratio.where(average > 0, other=1.0)         # cold start -> neutral 1.0

A z-score floors the standard deviation (MIN_STD) so a customer with zero

spending variance yields a finite score instead of infinity. Decide the

cold-start behaviour explicitly; do not let inf/NaN reach the model.

Entity design

  • One concept per entity. customer and merchant are separate entities

with separate join keys. A feature service can pull from both

(fraud_detection_v1 reads customer *and* merchant features) by listing

views for each.

  • Stable, integer-ish join keys. Keys are looked up on every online request;

keep them small and immutable. Do not key on something that changes (email).

  • Push the entity key into every source. A feature view can only serve an

entity whose key appears in its source rows.

Choosing a TTL

ttl bounds how far back online serving will look for a row before returning

None. Match it to the cadence at which the source updates:

Feature kindUpdate cadenceSuggested TTLThis package
Slowly-changing profileWeekly30-90 daysPROFILE_TTL = 90d
Rolling behavioural statsDaily / streamed1-3 daysTXN_STATS_TTL = 3d
Merchant aggregatesDaily~7 daysMERCHANT_TTL = 7d

Too short and fresh-enough values expire to None; too long and the model

serves stale data after a pipeline outage. The TTL is a freshness SLA -- treat

a sudden rise in None lookups as an alert that materialization is behind.

Push vs. batch ingestion

Batch materialization has a built-in lag equal to its schedule. For features

where that lag matters (transaction velocity during an attack), stream updates

through the push source and reserve batch for the slow-moving views:

python
push_micro_batch(store, "customer_txn_push_source", fresh_df, mode="online")

Use mode="online_and_offline" when the streamed value should also become part

of the training history; mode="online" when it is serving-only.

Backfills and reproducibility

When you add a feature, backfill it before training on it, or early rows will be

None:

python
materialize_window(store, days=180)   # populate history for the new view

Pin your Feast version and persist generated training sets

(churn_training_set.parquet in the example) so a model can always be retrained

on the exact data it originally saw.

A checklist before you ship a feature

  • [ ] Window is named in the feature (*_7d, *_30d).
  • [ ] Cold-start / null / zero behaviour is explicit and unit-tested.
  • [ ] Computed once and read via a FeatureService (no train/serve skew).
  • [ ] TTL matches the source's update cadence.
  • [ ] On-demand only if it genuinely depends on request-time inputs.
  • [ ] Backfilled far enough to cover your training window.
Feature Store Bootstrap v1.0.0 — Free Preview