Worked examples — Feature stores
This page drills the Feature stores parent note into concrete numbers. The parent gave you the point-in-time join rule and the skew condition. Here we grind every case where those rules bite — including the weird corners where a naive engineer ships a bug.
The scenario matrix
Every cell below is a distinct thing that can go wrong (or must be handled). Each worked example is tagged with the cell it covers.
| Cell | Case class | What varies | Danger if mishandled |
|---|---|---|---|
| A | Normal as-of join | falls between two observations | none — baseline |
| B | Future-leak corner | an observation exists after | data leakage |
| C | Exact-tie corner | an observation lands exactly on | off-by-one (" vs ") |
| D | Empty-past / cold start | no observation before | null must be returned, not crash |
| E | TTL expiry | freshest value is older than | stale feature lies to model |
| F | Multi-entity join | several users, one query | wrong-key cross-contamination |
| G | Skew (offline ≠ online) | two code paths compute differently | silent production degradation |
| H | Word problem (fraud) | full pipeline end-to-end | integrating all rules at once |
| I | Exam twist | latest-value shortcut vs as-of | reveals the classic trap |
We reuse two prerequisite ideas: Data leakage (cell B) and Training-serving skew (cell G).
The picture we compute against

Look at the figure. The horizontal axis is time. Each mint dot is a feature observation — a moment when we recorded a value. The coral vertical line at is the label event: the instant we must make a prediction. The as-of rule says: sweep leftward from the coral line and grab the first mint dot you meet. Everything to the right of the coral line is the forbidden future.
Throughout, we use one running table for user U:
| value | |
|---|---|
| 09:00 | $100 |
| 10:30 | $40 |
| 12:00 | $90 |
Cell A — the normal as-of join
Cell B — the future-leak corner

Cell C — the exact-tie corner ( vs )
Cell D — empty past / cold start
Event at for user U. Earliest observation is 09:00.
Forecast: what should the feature value be?
- Filter . Set is empty (09:00, 10:30, 12:00 all lie in the future relative to 08:00). Why this step? Same as-of rule; here it simply yields nothing.
- over an empty set is undefined → return null / default. Why not crash or guess? A brand-new user genuinely has no history; fabricating a number would be a lie. The model must have a defined behaviour for "no data yet" (a default like $0, or an is-null flag).
Verify: count of allowed observations , so value is null. Any non-null answer here would be inventing history. ✓
Cell E — TTL expiry (stale kill)
Event at , TTL min. Allowed window is .
Forecast: does $100 (from 09:00) survive?
- Check the side: 09:00 09:05 ✓ — it's in the past, good.
- Check the TTL side: is ? No — 09:00 is 5 minutes old, older than the 2-minute limit. Why this step? TTL asks "is this still fresh enough to trust?" A 5-min-old balance in a 2-min-TTL world is treated as unknown.
- Window is empty → null / default.
Verify: age min min, so the only past observation is expired; result null. ✓ Contrast Cell A (same 09:00 dot, no TTL) where it would have been a valid fallback.
Cell F — multi-entity join (don't cross the keys)
Now add user V: observations 08:00 → $5, 10:00 → $8. Query the balance for both U and V at .
Forecast: two numbers — guess them.
- Partition by entity key first. U's rows and V's rows are kept in separate buckets.
Why this step? The join key (
user_id) must gate the as-of search. If you as-of-join across all rows ignoring the key, U could inherit V's value — nonsense. - Run the as-of rule inside each bucket at :
- U: candidates 09:00, 10:30 → freshest 10:30 → $40.
- V: candidates 08:00, 10:00 → freshest 10:00 → $8.
Verify: U's answer matches Cell A ($40) ✓. For V, and , so $8 ✓. Neither borrowed the other's value. ✓
Cell G — the skew corner (offline ≠ online)
The feature is avg_balance = mean of a user's recorded balances. Offline (batch, Spark) computes it over all three of U's observations: 100, 40, 90. Online (real-time) mistakenly averages only the two most recent it has cached: 40, 90. What is the skew?
Forecast: by how much do the two paths disagree?
- Offline value: .
- Online value: . Why this step? We evaluate each code path on the same raw data to expose that .
- Skew . Why it's fatal? The model was trained on 76.67-style numbers but served 65.00-style numbers — the input distribution shifts silently. This is exactly the Training-serving skew the parent note warns about.
Verify: offline , online , difference . A feature store fixes this by materializing the offline result into the online store (Redis/DynamoDB), so both paths read one number and skew by construction. ✓
Cell H — real-world word problem (fraud, full pipeline)

A fraud model uses feature hours_since_last_topup. User U's top-ups happened at 09:00 and 10:30. A transaction (the label event) fires at . TTL h. Compute the served feature.
Forecast: guess the hour count before deriving.
- As-of the last top-up : that is 10:30. Why this step? We need what was known at 11:00; the 12:00 balance top-up (if any) is the future.
- TTL check: age h h ✓ — fresh, keep it.
- Compute the feature: h. Why this form? The transformation must be the same registered definition used offline in training, or we re-introduce Cell G skew.
Verify: h, positive (event after top-up) ✓, and within TTL ✓. Units are hours ✓. Served value 0.5 h.
Cell I — the exam twist (latest-value trap)
An exam asks: for a historical label at , a student reuses the online store's latest value (which today is $90 from 12:00) instead of doing an as-of join. What value should they have used, and what is the error?
Forecast: correct value? magnitude of the mistake?
- Correct as-of at : candidates are just 09:00 → $100. Why? 10:30 and 12:00 are both after 10:00, forbidden.
- Student's latest-value pick: $90 (from 12:00, three hours in the future).
- Error: , and worse, it's a leak (the $90 encodes future behaviour). Why the trap is seductive? The online store makes "latest" trivially easy — but "latest today" ≠ "latest as of that past label." This is the parent note's Mistake #2 in numbers.
Verify: as-of value =\100t_e=10{:}00=$90=$10\ne 0> 10{:}00$ proves it's a future leak. ✓
Recall Which cell breaks each way?
Future leak comes from forgetting ::: the filter (Cell B, Cell I) A tie at should be ::: included, because is used, not (Cell C) No past observation means the feature is ::: null / default, never fabricated (Cell D) A value older than TTL is treated as ::: unknown / expired (Cell E) Offline mean 76.67 vs online mean 65 is an example of ::: training–serving skew (Cell G)
"FILTER-FRESH-FLAG" — Filter to (no future), take the Freshest within TTL, and if the set is empty raise a null Flag. Every worked example above is one of these three failing or succeeding.