Skip to content

01 · Production ML System Design

Everything so far has ended at a trained model in a notebook. A production ML system is a set of connected pipelines: data flows in, features get computed consistently, models get trained and served, and predictions feed back into monitoring. This module maps that architecture and its most common failure mode — training/serving skew.

The anatomy of a production ML system

   [Raw data sources]
          |
   [Feature pipeline] ---> [Feature store] ---> [Online feature lookup]
          |                                             |
   [Training pipeline]                          [Serving/inference API]
          |                                             |
   [Model registry]  --------(deploy)-----------> [Production model]
          |                                             |
   [Offline evaluation]                         [Online monitoring/logging]
                                                          |
                                              [Feedback -> new training data]

Every arrow is a place where a bug or a mismatch can quietly enter the system — this module focuses on the training/serving boundary, the single most common source of production ML failures.

Training/serving skew: the same bug, two codebases

import pandas as pd

def compute_features_training(df: pd.DataFrame) -> pd.DataFrame:
    """Used once, in batch, over the full historical dataset."""
    out = df.copy()
    out["avg_purchase_30d"] = out.groupby("user_id")["amount"].transform(
        lambda s: s.rolling(30, min_periods=1).mean()
    )
    out["days_since_signup"] = (out["event_time"] - out["signup_time"]).dt.days
    return out

def compute_features_serving(user_row: dict) -> dict:
    """Used per-request, at inference time -- often reimplemented separately."""
    features = dict(user_row)
    # BUG: this recomputation used a *90-day* window, not 30 -- easy to miss in review
    features["avg_purchase_30d"] = user_row.get("avg_purchase_90d_cache", 0.0)
    features["days_since_signup"] = (pd.Timestamp.now() - user_row["signup_time"]).days
    return features

The training pipeline computes avg_purchase_30d correctly over a 30-day window; the serving path, written separately for latency reasons, silently substitutes a 90-day cached value. The model was trained on one distribution of that feature and served a different one — with no error, no crash, and no signal beyond degraded live accuracy that's hard to localize.

Worked example: catching skew before it ships

def check_feature_parity(training_fn, serving_fn, sample_rows, tolerance=1e-6):
    mismatches = []
    for row in sample_rows:
        train_val = training_fn(pd.DataFrame([row])).iloc[0].to_dict()
        serve_val = serving_fn(row)
        for key in train_val:
            if key in serve_val:
                diff = abs(train_val[key] - serve_val[key]) if isinstance(train_val[key], (int, float)) else None
                if diff is not None and diff > tolerance:
                    mismatches.append((key, train_val[key], serve_val[key]))
    return mismatches

# A parity test run in CI against a sample of real rows, comparing both code paths
# on identical inputs -- would have caught the 30d vs 90d bug above before deploy.

The single-pipeline alternative

The most reliable fix is architectural, not procedural: compute features with one shared function/library, called both by the offline training pipeline and the online serving path (often via a feature store, covered next module).

def compute_avg_purchase(purchases: pd.DataFrame, window_days: int = 30) -> float:
    """The ONE implementation, imported by both training and serving code."""
    cutoff = purchases["event_time"].max() - pd.Timedelta(days=window_days)
    return purchases[purchases["event_time"] >= cutoff]["amount"].mean()

Cheat sheet

Failure mode Cause Fix
Training/serving skew Feature logic duplicated in two codebases Shared feature functions / feature store
Silent data drift Input distribution shifts after deploy Monitoring (Module 05)
Stale features at inference Online store lags the source data Freshness SLAs, staleness checks
"Works in the notebook" No parity test between offline/online paths Parity tests in CI

How It Actually Works

Training/serving skew degrades a model without changing a single weight, because it changes the input distribution the fixed weights are applied to. A trained model is a fixed function f(x) learned to be accurate for inputs x drawn from the training feature distribution. If the serving path computes a feature differently (30-day window vs. 90-day), the resulting x_serving is drawn from a different distribution than x_training — the model's learned weights, tuned to the statistical relationships in the 30-day version of that feature, are now being applied to values that follow different statistics. No code crashes and no metric alarms fire at the feature-computation layer, because both implementations produce syntactically valid numbers — the degradation only shows up several layers downstream, in prediction quality, which is exactly why this failure mode is notoriously hard to trace back to its root cause.

A parity test mechanically converts an architectural risk into a testable assertion by running both code paths on identical inputs and diffing the outputs. check_feature_parity takes the same raw row and feeds it through both the training feature function and the serving feature function, then compares each shared feature key numerically. Because both functions receive identical inputs, any discrepancy in output must come from a difference in the transformation logic itself (different window, different rounding, different null-handling) rather than from different input data — isolating the exact class of bug that would otherwise manifest only as unexplained accuracy degradation weeks after deployment. Running this as an automated CI check on a sample of real rows means the 30-day-vs-90-day bug above would fail a test before merge, rather than being discovered via a slow, expensive investigation into "why did accuracy quietly drop."

A shared feature function structurally eliminates skew by making divergence impossible, not merely detected. compute_avg_purchase called from both the training pipeline (in batch, over historical data) and the serving path (per-request, over recent data) uses the literal same code — there is no second implementation that could drift out of sync with the first, because there is only one implementation. This is the architectural principle a feature store (Module 03) generalizes: rather than relying on discipline (remembering to update both copies whenever logic changes) or detection (parity tests catching drift after the fact), it removes the duplication that makes skew possible in the first place — the online and offline paths both read from, or are both computed by, the same underlying feature definition.

Exercise

Take the buggy compute_features_serving function above and rewrite it to call the shared compute_avg_purchase function instead of the cached 90-day value. Then write a small check_feature_parity-style test with 3 sample rows (varying purchase history) that would have failed against the original buggy version and passes against your fix — demonstrating the parity-test workflow end to end.