Case study 04 / ML Engineering · Software Engineering
ML features without future leakage.
A Go feature store with historical training joins, Parquet offline storage, and Redis online serving.
The problem
A model can look excellent offline if its training features contain information that only became available after a prediction event. How can a feature store prevent that leakage?
What I built
Built a YAML feature registry, offline Parquet storage, online Redis serving, materialization jobs, and an HTTP API for online and historical feature retrieval. A PySpark path supports larger feature-computation workloads.
How it works
The registry describes entities, sources, and feature TTLs. Materialization populates storage. Historical retrieval groups feature rows by entity and selects the latest eligible timestamp for each event; online retrieval reads materialized values from Redis.
- Feature registry
- Materialization
- Parquet / Redis
- Training / inference
Engineering decisions
Time is part of correctness
Historical joins reject feature timestamps after the event and rows older than the configured TTL. An event with no eligible feature retains missing features rather than borrowing a future value.
Make serving paths explicit
Separate online lookup and historical join endpoints keep low-latency inference retrieval distinct from training-data construction.
Test consistency
Dedicated tests cover point-in-time joins, registry validation, offline storage, online storage, and consistency between serving paths.
Results & evidence
Core guarantee
No future rows
Enforced by explicit timestamp and TTL checks in the historical join implementation; this is an implementation property, not a performance benchmark.
Storage paths
Offline + online
Parquet for historical retrieval and Redis for online lookup, with a shared registry.
Results are documented in the linked project artifacts. They have not been independently reproduced for this portfolio.
Limits & lessons
The historical join implementation reads source rows into memory and scans each entity’s rows per event. Spark materialization scale does not establish historical-join latency or online-serving capacity.
Training-serving consistency requires explicit time semantics, not just matching column names.