← All projects

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.

  • Go
  • Redis
  • Parquet
  • PySpark

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.

  1. Feature registry
  2. Materialization
  3. Parquet / Redis
  4. Training / inference
A simplified flow; implementation details are linked below.

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.

Source material