Model Registry and Versioning

Suraj Kumar

Suraj Kumar

Last updated November 5, 2026·18 min read

MLOps course · lesson 13 of 24
  1. 1What Is MLOps? From Notebooks to Reliable Production
  2. 2MLOps Principles and Maturity: From Manual to Automated
  3. 3ML Fundamentals for MLOps: Splits, Leakage, and Metrics
  4. 4A Reproducible ML Project: Layout, Seeds, and Data Code
  5. 5Data Engineering Basics for ML: Ingestion, SQL, and Skew
  6. 6Pipeline Orchestration: Apache Airflow and Kubeflow
  7. 7Batch vs Streaming: Spark, Kafka, and Flink
  8. 8Data Versioning and Lineage: DVC and Beyond
  9. 9Data Validation and Quality in ML Pipelines
  10. 10Experiment Tracking with MLflow
  11. 11Reproducible Training Pipelines
  12. 12Model Evaluation and Validation Gates
  13. 13Model Registry and Versioning
  14. 14CI/CD for Machine Learning
  15. 15Containers and Kubernetes for ML
  16. 16Infrastructure as Code: Terraform and Ansible
  17. 17Cloud ML Platforms: AWS, Azure, GCP, and Self-Built
  18. 18Model Serving Patterns: Batch, Online, and Streaming
  19. 19Inference Engineering Basics: Latency, Batching, and Cost
  20. 20Monitoring and Observability for ML Models
  21. 21Drift Detection and Retraining for ML Models
  22. 22Explainable AI: LIME, SHAP, and Model Interpretability
  23. 23Edge AI Deployment: Jetson, TFLite, and Mobile ML
  24. 24Capstone: An End-to-End MLOps Pipeline
View full syllabus

In traditional software release management, application binaries are versioned, tagged, and published to an artifact repository such as Docker Hub, Artifactory, or npm. Production environments never pull an unversioned latest tag; they deploy explicit, immutable versions like v2.4.1 backed by audit trails and automated rollback procedures.

In early-stage machine learning teams, model management often resembles the wild west. A data scientist trains a model, saves a pickle file named churn_model_final_v2.pkl to a shared network drive or S3 bucket, and sends a Slack message to backend engineering: "The new model is ready in S3, please update the API service."

This ad-hoc approach creates catastrophic operational vulnerabilities. Nobody knows which code commit produced the file, which training dataset trained it, whether it passed statistical validation gates, or who authorized its deployment. If the new model spikes API latency or triggers false churn alerts on Monday morning, rolling back requires frantically digging through chat history to locate last week's file path.

To operationalize machine learning, you must decouple model training from model deployment. That central decoupling point is the Model Registry.

In this lesson, you will learn the architecture, storage layouts, and governance of model registries. You will explore model versioning, lifecycle stages and pointer aliases, automated promotion rules, model signatures, zero-downtime rollback strategies, and implement a fully tested model registry in Python.

The StayWell figures in this lesson (rates, scores, counts and prices) are made up for illustration. They show how to read the numbers, not what real systems achieve.

What a model registry does

A Model Registry is a centralized, version-controlled repository designed specifically for storing, managing, and governing trained machine learning models throughout their operational lifecycle.

Model registry lifecycle showing progression from registered candidate to staging, active production champion, and rollback vault

A model registry provides four foundational operational capabilities:

1. Centralized model catalog

Rather than scattering files across developer laptops and cloud storage buckets, the registry organizes models under standardized, unique business names (such as staywell-customer-churn or staywell-dynamic-pricing). Every trained iteration of that model is automatically assigned a monotonically increasing integer version (v1, v2, v3).

2. Full lineage provenance

A registered model version is never an isolated binary file. It is a cryptographic junction that binds the trained artifact to its complete history:

  • The MLflow experiment run ID that generated the model
  • The Git commit SHA of the training pipeline repository
  • The content hash of the training dataset
  • The evaluation gate certificate proving non-regression
  • The container environment manifest

The model version graph linking artifacts to runs, code commits, dataset hashes, and approval tokens

3. Lifecycle aliases and pointer decoupling

In traditional architectures, changing which model runs in production required editing microservice configuration files or re-deploying Kubernetes pods with updated file paths.

A modern model registry decouples the serving application from physical versions through Lifecycle Aliases (such as @champion, @challenger, and @previous-champion). The production inference service is configured once:

python
1# The serving service queries an alias, not a hardcoded version number
2model = registry.load_model("staywell-customer-churn", alias="@champion")

When a new model is promoted, the registry simply shifts the @champion pointer from version 2 to version 3. The serving service dynamically loads the updated artifact without requiring a code commit, configuration re-deployment, or container restart.

4. Auditable governance and promotion gates

In regulated industries and mature engineering organizations, deploying a machine learning model is an auditable event. The registry enforces access control and transition criteria: a model cannot be tagged as @champion unless it carries an automated evaluation pass token and cryptographic sign-off from an authorized lead engineer.

Modern registry conventions: aliases over legacy stages

Historically, early versions of tools like MLflow organized models using static lifecycle Stages: None, Staging, Production, and Archived.

The industry has moved decisively away from hardcoded stages toward Model Aliases and Tags.

The official MLflow Model Registry Documentation (verified on 4 November 2026) explains why aliases superseded legacy stages:

  • Multiple Deployment Targets: A single model might serve multiple operational contexts simultaneously (for example, @us-east-prod, @eu-west-prod, and @mobile-edge). Static stages could not represent multi-region rollouts.
  • Champion-Challenger Routing: Testing two models in production simultaneously requires distinct semantic tags (@champion receiving 90% of traffic and @challenger receiving 10% shadow traffic) rather than a single Production bucket.
  • Custom Environments: Aliases allow engineering teams to define custom lifecycle workflows that mirror their infrastructure (such as @dev, @integration-test, @canary, and @previous-champion).

Model signatures: enforcing schema contracts at the perimeter

One of the most frequent causes of silent production failures is schema mismatch between the training environment and the live inference application. A model trained with three numerical columns and one categorical string will crash or misbehave if an upstream API sends an integer where a float was expected, or renames a key from tenure_months to tenure.

A Model Signature defines the formal input and output contract of a model version within the registry.

In MLflow and enterprise registries, signatures define:

  1. Inputs (Columns and Types): The required feature names and their exact primitive data types (such as double, integer, string, boolean).
  2. Outputs (Format and Dimensions): The shape and type of the generated prediction (for example, a single floating-point churn probability between 0.0 and 1.0, or a multi-class probability array).
  3. Optional Parameters: Inference-time parameters that modify prediction behavior without altering model weights, such as custom decision cutoffs.

When an inference service receives a prediction request, the model registry runtime validates the incoming JSON payload against the registered model signature before executing the underlying scoring function. If a client transmits missing fields or illegal data types, the request is rejected immediately at the gateway perimeter with an informative client error, preventing corrupted data from reaching the mathematical model.

Enterprise model registries: cloud ecosystem comparison

While open-source MLflow provides the reference architecture for self-hosted registries, cloud providers offer managed implementations integrated directly into their broader cloud ecosystems:

Amazon SageMaker Model Registry

The official AWS SageMaker Documentation (verified on 4 November 2026) describes SageMaker Model Registry as a specialized catalog for managing model packages. In SageMaker:

  • Models are grouped into Model Package Groups.
  • Approval statuses (Approved, PendingManualApproval, Rejected) control whether a model version can be deployed to SageMaker real-time endpoints or batch transform jobs.
  • IAM role policies restrict approval permissions, ensuring data scientists cannot promote their own models without cross-account authorization from security leads.

Google Cloud Vertex AI Model Registry

The official Google Cloud Vertex AI Documentation (verified on 4 November 2026) highlights its centralized repository for managing the lifecycle of ML models. Vertex AI provides:

  • First-class support for versioning AutoML and custom container models.
  • Native integration with Vertex AI Model Monitoring to track drift against the training dataset baseline registered alongside the model.
  • Direct endpoint deployment mapping where multiple model versions share traffic splits behind a single global REST URL.

Databricks Unity Catalog

Databricks has unified model governance with data governance through Unity Catalog. In Unity Catalog, models are treated as first-class securable assets alongside tables, volumes, and views. Permissions use standard SQL grant syntax (such as GRANT EXECUTE ON MODEL prod.staywell_churn TO 'api-service'), providing unified data lineage from raw Lakehouse Delta tables directly to production inference artifacts.

Deployment patterns enabled by the registry

An automated model registry makes safer deployment strategies possible:

Shadow deployments (Dark traffic)

In a shadow deployment, the live production application receives customer requests and forwards a copy of the input payload to both the active @champion model and the new @challenger candidate model.

  • The @champion prediction is returned to the user.
  • The @challenger prediction is logged to a monitoring database alongside latency and resource utilization metrics.
  • Business operations are completely insulated: if the challenger experiences memory leaks, unhandled exceptions, or latency spikes, real customers never notice.

Canary rollouts (Progressive traffic shifting)

Once a challenger demonstrates stability in shadow mode, the registry coordinates with the API gateway or service mesh to route a small slice of real traffic to the candidate:

  1. Day 1: 5% of incoming requests routed to candidate version; 95% to champion.
  2. Day 2: Monitor error rates, latency percentiles, and conversion metrics in Grafana.
  3. Day 3: If no regressions occur, scale candidate traffic to 25%, then 50%, and finally 100%.
  4. Day 4: Promote candidate version to @champion in the registry and re-assign the old model to @previous-champion.

Zero-downtime rollback mechanics

The true test of an MLOps platform is not how fast it deploys a new model; it is how safely and quickly it recovers when a newly deployed model fails.

Imagine model v2.4.0 was promoted to @champion at 02:00 UTC. At 08:30 UTC, customer support reports that reservation confirmation emails are dropping because the churn classifier is timing out on complex user profiles.

If your team relies on re-training or re-building Docker containers, mitigating the incident takes thirty minutes to an hour. In a mature model registry with pointer-based rollback:

  1. The registry always preserves the displaced champion under the @previous-champion alias.
  2. The on-call engineer or automated canary monitor triggers a rollback command:
    bash
    1python -m src.registry rollback --model staywell-customer-churn --reason "p95 latency spike"
  3. The registry shifts the @champion pointer back to the previous version.
  4. The live API picks up the previous version within seconds, instantly restoring healthy service while engineers investigate the root cause offline.

Operational template: model promotion checklist

Every production promotion should satisfy an explicit, auditable checklist before the registry pointer shifts.

markdown
1# StayWell Model Promotion and Release Checklist
2# Model: staywell-customer-churn
3# Nominated Version: v2.4.0
4# Review Date: 2026-11-05
5
6## 1. Automated Verification Prerequisites
7- [x] Code Quality: Training pipeline unit tests passing (100% test pass rate)
8- [x] Data Contract: Input feature schema validated against DC-STAYWELL-CHURN-FEATURES-V1
9- [x] Provenance: Git commit SHA (a94f8e12) and DVC data hash (7b9c1d0f) locked
10- [x] Evaluation Gate: Passed all 4 layers (Absolute AUC, Champion Parity, Slices, Calibration)
11- [x] Gate Token: GATE-PASS-20261105-001 registered in run metadata
12
13## 2. Operational Benchmarks
14- [x] Inference Latency: p95 latency measured at 1.8 ms (SLA ceiling: 5.0 ms)
15- [x] Container Footprint: Memory size under 50 MB (Actual: 14.2 MB)
16- [x] Staging Shadow Validation: Ran on 10% shadow traffic for 24 hours with zero 500 errors
17
18## 3. Deployment and Rollback Strategy
19- [x] Target Alias: `@champion`
20- [x] Fallback Version: v2.3.0 designated as `@previous-champion`
21- [x] Rollback Runbook: Confirmed zero-downtime pointer reversal procedure
22- [x] Monitoring Alerts: Drift monitors and latency thresholds active in Grafana
23
24## 4. Governance Sign-off
25- Technical Owner: Machine Learning Engineer (Automated CI/CD Pipeline)
26- Approving Authority: Lead MLOps Engineer (Lead ML Engineer, 5 November 2026)
27- Promotion Action: APPROVED FOR PRODUCTION

Hands-on implementation: a tested Python model registry

To understand the internal mechanics of a model registry, let us build a production-grade, file-based model registry using the Python standard library.

This implementation provides:

  • Sequential version tracking per model namespace
  • Metadata bound to each version (run ID, Git commit SHA, dataset hash, status)
  • Flexible lifecycle aliases (@champion, @challenger, @previous-champion)
  • Enforced promotion gates requiring valid verification tokens and approver IDs
  • Automated preservation of previous champions for instant rollback
  • Complete JSON persistence with an immutable audit log

The registry implementation (model_registry.py)

Here is the complete implementation:

model_registry.py

python
1"""Lightweight, production-grade model registry with promotion and rollback rules.
2
3Implements model versioning, lifecycle aliases (@champion, @challenger, @previous-champion),
4cryptographic provenance linking, and auditable promotion/rollback workflows.
5"""
6
7from __future__ import annotations
8
9import dataclasses
10import json
11import time
12from pathlib import Path
13from typing import Any, Dict, List, Optional, Set
14
15
16@dataclasses.dataclass
17class ModelVersion:
18    """Represents an immutable model version entry in the registry."""
19    model_name: str
20    version: int
21    artifact_uri: str
22    run_id: str
23    git_commit: str
24    dataset_hash: str
25    status: str
26    aliases: Set[str] = dataclasses.field(default_factory=set)
27    metadata: Dict[str, Any] = dataclasses.field(default_factory=dict)
28    created_at: float = dataclasses.field(default_factory=time.time)
29
30    def to_dict(self) -> Dict[str, Any]:
31        return {
32            "model_name": self.model_name,
33            "version": self.version,
34            "artifact_uri": self.artifact_uri,
35            "run_id": self.run_id,
36            "git_commit": self.git_commit,
37            "dataset_hash": self.dataset_hash,
38            "status": self.status,
39            "aliases": sorted(list(self.aliases)),
40            "metadata": self.metadata,
41            "created_at": self.created_at,
42        }
43
44    @classmethod
45    def from_dict(cls, data: Dict[str, Any]) -> ModelVersion:
46        aliases = set(data.get("aliases", []))
47        return cls(
48            model_name=data["model_name"],
49            version=data["version"],
50            artifact_uri=data["artifact_uri"],
51            run_id=data["run_id"],
52            git_commit=data["git_commit"],
53            dataset_hash=data["dataset_hash"],
54            status=data["status"],
55            aliases=aliases,
56            metadata=data.get("metadata", {}),
57            created_at=data.get("created_at", time.time()),
58        )
59
60
61class ModelRegistry:
62    """Manages registered models, version progressions, and governance workflows."""
63
64    def __init__(self, registry_file: str) -> None:
65        self.file_path = Path(registry_file)
66        self.models: Dict[str, Dict[int, ModelVersion]] = {}
67        self.audit_log: List[Dict[str, Any]] = []
68        self._load()
69
70    def register_version(
71        self,
72        model_name: str,
73        artifact_uri: str,
74        run_id: str,
75        git_commit: str,
76        dataset_hash: str,
77        metadata: Optional[Dict[str, Any]] = None,
78    ) -> ModelVersion:
79        """Register a newly trained model artifact version."""
80        if model_name not in self.models:
81            self.models[model_name] = {}
82
83        next_version = len(self.models[model_name]) + 1
84        mv = ModelVersion(
85            model_name=model_name,
86            version=next_version,
87            artifact_uri=artifact_uri,
88            run_id=run_id,
89            git_commit=git_commit,
90            dataset_hash=dataset_hash,
91            status="REGISTERED",
92            aliases=set(),
93            metadata=metadata or {},
94        )
95        self.models[model_name][next_version] = mv
96        self._record_audit(
97            event_type="REGISTER_VERSION",
98            model_name=model_name,
99            version=next_version,
100            details={"run_id": run_id, "git_commit": git_commit},
101        )
102        self._save()
103        return mv
104
105    def set_alias(self, model_name: str, alias: str, version: int) -> None:
106        """Assign a pointer alias (@champion, @challenger) to a specific model version."""
107        if model_name not in self.models or version not in self.models[model_name]:
108            raise KeyError(f"Model {model_name} version {version} not found.")
109
110        # Remove alias from any existing version
111        for v in self.models[model_name].values():
112            v.aliases.discard(alias)
113
114        self.models[model_name][version].aliases.add(alias)
115        self._save()
116
117    def get_model_by_alias(self, model_name: str, alias: str) -> Optional[ModelVersion]:
118        """Fetch the model version currently associated with an alias."""
119        clean_alias = alias if alias.startswith("@") else f"@{alias}"
120        if model_name not in self.models:
121            return None
122        for v in self.models[model_name].values():
123            if clean_alias in v.aliases:
124                return v
125        return None
126
127    def get_version(self, model_name: str, version: int) -> Optional[ModelVersion]:
128        """Retrieve a specific version by its numeric identifier."""
129        return self.models.get(model_name, {}).get(version)
130
131    def promote_to_champion(
132        self,
133        model_name: str,
134        candidate_version: int,
135        gate_token: str,
136        approved_by: str,
137    ) -> ModelVersion:
138        """Promote a candidate version to @champion after verifying gate pass and approvals."""
139        if not gate_token or not gate_token.startswith("GATE-PASS"):
140            raise ValueError(f"Invalid or missing evaluation gate token: {gate_token}")
141        if not approved_by:
142            raise ValueError("Promotion requires an authorized approver username.")
143
144        candidate = self.get_version(model_name, candidate_version)
145        if not candidate:
146            raise KeyError(f"Candidate version {candidate_version} not found.")
147
148        # Demote current champion to @previous-champion for instant rollback
149        current_champ = self.get_model_by_alias(model_name, "@champion")
150        if current_champ and current_champ.version != candidate_version:
151            self.set_alias(model_name, "@previous-champion", current_champ.version)
152            current_champ.status = "ARCHIVED_CHAMPION"
153
154        # Promote candidate
155        self.set_alias(model_name, "@champion", candidate_version)
156        candidate.status = "PRODUCTION"
157        candidate.metadata["promoted_at"] = time.time()
158        candidate.metadata["approved_by"] = approved_by
159        candidate.metadata["gate_token"] = gate_token
160
161        self._record_audit(
162            event_type="PROMOTE_TO_CHAMPION",
163            model_name=model_name,
164            version=candidate_version,
165            details={
166                "gate_token": gate_token,
167                "approved_by": approved_by,
168                "previous_champion": current_champ.version if current_champ else None,
169            },
170        )
171        self._save()
172        return candidate
173
174    def rollback_champion(
175        self,
176        model_name: str,
177        reason: str,
178        executed_by: str,
179    ) -> ModelVersion:
180        """Instantly restore the @champion alias to @previous-champion."""
181        prev_champ = self.get_model_by_alias(model_name, "@previous-champion")
182        if not prev_champ:
183            raise RuntimeError("No previous champion found in registry for rollback.")
184
185        current_champ = self.get_model_by_alias(model_name, "@champion")
186        current_version = current_champ.version if current_champ else None
187
188        # Reassign @champion to previous champion
189        self.set_alias(model_name, "@champion", prev_champ.version)
190        prev_champ.status = "PRODUCTION"
191
192        if current_champ:
193            current_champ.aliases.discard("@champion")
194            current_champ.status = "ROLLED_BACK"
195            current_champ.metadata["rollback_reason"] = reason
196
197        self._record_audit(
198            event_type="ROLLBACK_CHAMPION",
199            model_name=model_name,
200            version=prev_champ.version,
201            details={
202                "rolled_back_from": current_version,
203                "reason": reason,
204                "executed_by": executed_by,
205            },
206        )
207        self._save()
208        return prev_champ
209
210    def _record_audit(self, event_type: str, model_name: str, version: int, details: Dict[str, Any]) -> None:
211        self.audit_log.append({
212            "timestamp": time.time(),
213            "event_type": event_type,
214            "model_name": model_name,
215            "version": version,
216            "details": details,
217        })
218
219    def _save(self) -> None:
220        self.file_path.parent.mkdir(parents=True, exist_ok=True)
221        payload = {
222            "models": {
223                name: {str(v_num): mv.to_dict() for v_num, mv in v_dict.items()}
224                for name, v_dict in self.models.items()
225            },
226            "audit_log": self.audit_log,
227        }
228        with open(self.file_path, "w", encoding="utf-8") as f:
229            json.dump(payload, f, indent=2)
230
231    def _load(self) -> None:
232        if not self.file_path.is_file():
233            return
234        with open(self.file_path, "r", encoding="utf-8") as f:
235            payload = json.load(f)
236
237        self.models = {}
238        for name, v_dict in payload.get("models", {}).items():
239            self.models[name] = {int(v_num): ModelVersion.from_dict(d) for v_num, d in v_dict.items()}
240        self.audit_log = payload.get("audit_log", [])

The unit test suite (model_registry_test.py)

Here is the test suite validating sequential versioning, alias progression, gate token requirements, instant rollbacks, and file persistence:

model_registry_test.py

python
1"""Unit tests for the MLOps model registry."""
2
3import shutil
4import tempfile
5import unittest
6from pathlib import Path
7from model_registry import ModelRegistry
8
9
10class TestModelRegistry(unittest.TestCase):
11
12    def setUp(self):
13        self.test_dir = tempfile.mkdtemp(prefix="test_registry_")
14        self.registry_file = Path(self.test_dir) / "registry.json"
15        self.registry = ModelRegistry(str(self.registry_file))
16
17    def tearDown(self):
18        shutil.rmtree(self.test_dir)
19
20    def test_register_and_retrieve_versions(self):
21        v1 = self.registry.register_version(
22            model_name="staywell-churn",
23            artifact_uri="s3://staywell-artifacts/models/v1.joblib",
24            run_id="run_101",
25            git_commit="a94f8e12",
26            dataset_hash="7b9c1d0f",
27            metadata={"auc": 0.82},
28        )
29        self.assertEqual(v1.version, 1)
30        self.assertEqual(v1.status, "REGISTERED")
31
32        v2 = self.registry.register_version(
33            model_name="staywell-churn",
34            artifact_uri="s3://staywell-artifacts/models/v2.joblib",
35            run_id="run_102",
36            git_commit="c310ef99",
37            dataset_hash="7b9c1d0f",
38            metadata={"auc": 0.85},
39        )
40        self.assertEqual(v2.version, 2)
41
42        retrieved_v1 = self.registry.get_version("staywell-churn", 1)
43        self.assertIsNotNone(retrieved_v1)
44        self.assertEqual(retrieved_v1.git_commit, "a94f8e12")
45
46    def test_promote_to_champion_lifecycle(self):
47        # Register v1 and promote to champion
48        self.registry.register_version(
49            model_name="staywell-churn",
50            artifact_uri="s3://staywell-artifacts/models/v1.joblib",
51            run_id="run_101",
52            git_commit="a94f8e12",
53            dataset_hash="7b9c1d0f",
54        )
55        self.registry.promote_to_champion(
56            model_name="staywell-churn",
57            candidate_version=1,
58            gate_token="GATE-PASS-20261105-001",
59            approved_by="lead-mle",
60        )
61
62        champ = self.registry.get_model_by_alias("staywell-churn", "@champion")
63        self.assertIsNotNone(champ)
64        self.assertEqual(champ.version, 1)
65        self.assertEqual(champ.status, "PRODUCTION")
66
67        # Register v2 and promote to champion
68        self.registry.register_version(
69            model_name="staywell-churn",
70            artifact_uri="s3://staywell-artifacts/models/v2.joblib",
71            run_id="run_102",
72            git_commit="b421a881",
73            dataset_hash="7b9c1d0f",
74        )
75        self.registry.promote_to_champion(
76            model_name="staywell-churn",
77            candidate_version=2,
78            gate_token="GATE-PASS-20261105-002",
79            approved_by="lead-mle",
80        )
81
82        new_champ = self.registry.get_model_by_alias("staywell-churn", "@champion")
83        self.assertEqual(new_champ.version, 2)
84
85        prev_champ = self.registry.get_model_by_alias("staywell-churn", "@previous-champion")
86        self.assertIsNotNone(prev_champ)
87        self.assertEqual(prev_champ.version, 1)
88
89    def test_instant_rollback(self):
90        # Setup: v1 is old champ, v2 is current champ
91        self.registry.register_version("staywell-churn", "s3://v1.joblib", "r1", "c1", "d1")
92        self.registry.promote_to_champion("staywell-churn", 1, "GATE-PASS-001", "alex")
93
94        self.registry.register_version("staywell-churn", "s3://v2.joblib", "r2", "c2", "d1")
95        self.registry.promote_to_champion("staywell-churn", 2, "GATE-PASS-002", "alex")
96
97        # Rollback champion due to production anomaly
98        restored = self.registry.rollback_champion(
99            model_name="staywell-churn",
100            reason="p95 latency spike on checkout API",
101            executed_by="oncall-mle",
102        )
103
104        self.assertEqual(restored.version, 1)
105        current_active = self.registry.get_model_by_alias("staywell-churn", "@champion")
106        self.assertEqual(current_active.version, 1)
107
108        v2_status = self.registry.get_version("staywell-churn", 2)
109        self.assertEqual(v2_status.status, "ROLLED_BACK")
110        self.assertNotIn("@champion", v2_status.aliases)
111
112    def test_promotion_requires_valid_gate_token(self):
113        self.registry.register_version("staywell-churn", "s3://v1.joblib", "r1", "c1", "d1")
114
115        with self.assertRaises(ValueError):
116            self.registry.promote_to_champion(
117                model_name="staywell-churn",
118                candidate_version=1,
119                gate_token="INVALID-TOKEN",
120                approved_by="alex",
121            )
122
123    def test_persistence_across_instances(self):
124        self.registry.register_version("staywell-churn", "s3://v1.joblib", "r1", "c1", "d1")
125        self.registry.promote_to_champion("staywell-churn", 1, "GATE-PASS-001", "alex")
126
127        # Reopen from disk in a fresh instance
128        fresh_registry = ModelRegistry(str(self.registry_file))
129        champ = fresh_registry.get_model_by_alias("staywell-churn", "@champion")
130        self.assertIsNotNone(champ)
131        self.assertEqual(champ.version, 1)
132        self.assertEqual(len(fresh_registry.audit_log), 2)
133
134
135if __name__ == "__main__":
136    unittest.main()

Save both files in the same folder and run the tests from that folder:

bash
1python3 -m unittest discover -p '*_test.py'

Every test runs in sub-millisecond execution time, ensuring that version progression and rollback behavior can be verified in automated CI suites.

Mistakes beginners make

1. Hardcoding file paths in serving applications

Beginners often configure inference microservices to load models from hardcoded file paths like s3://models/churn_model_v2.pkl. Updating models requires editing configuration files, rebuilding containers, and managing rolling deployments across clusters.

Fix: Load models by registry alias (such as @champion or @production). The serving service queries the registry endpoint to resolve the active artifact location, enabling instant zero-downtime model updates.

2. Overwriting existing model versions

Some teams save new model iterations under the same filename or database ID, effectively overwriting historical artifacts. If an issue surfaces, the previous weights are gone, making reproducing past behavior impossible.

Fix: Treat model versions as strictly immutable. Every training run registers a new, sequentially numbered version (v1, v2, v3). Never overwrite or delete historical production artifacts.

3. Promoting models without automated gate tokens

Allowing developers to manually promote models via web UI buttons without programmatic verification leads to accidental deployments of unvalidated prototypes.

Fix: Restrict registry promotion permissions. Enforce an automated check that requires a valid, cryptographically signed evaluation gate token before the registry permits an alias transition to @champion.

4. Forgetting to test the rollback procedure

Teams often plan for model deployment but fail to verify whether their rollback mechanism functions under pressure. When an incident occurs, they discover that the previous model artifact was pruned or that dependencies have shifted.

Fix: Automatically designate the displaced champion as @previous-champion during every promotion. Schedule quarterly "fire drill" exercises to test rolling back the active model in staging and production environments.

Recap and try it yourself

In this lesson, you explored how model registries bridge the gap between model training and production serving:

  • A model registry provides a centralized catalog, full provenance tracking, and decoupled lifecycle management.
  • Modern registries utilize dynamic aliases (such as @champion, @challenger, and @previous-champion) rather than legacy static stages.
  • Model signatures enforce strict input feature schemas and output formats at the perimeter, preventing runtime crashes.
  • Decoupling serving applications from specific version numbers enables zero-downtime promotions and sub-second rollbacks.
  • An auditable promotion checklist ensures that code tests, data contracts, evaluation gates, and governance approvals are verified prior to release.
  • A tested, file-based Python registry demonstrates how version progression, alias management, and rollback mechanisms operate under the hood.

Try it yourself

To practice model registry management on your local machine:

  1. Run the registry test suite: Copy model_registry.py and model_registry_test.py into a local directory and run python3 -m unittest model_registry_test.py. Verify that all five unit tests pass.
  2. Implement an @canary alias: Extend ModelRegistry to support an @canary alias that receives a percentage weight in metadata (for example, routing 10% of traffic to a new version).
  3. Simulate an emergency rollback: Write a Python script that registers three model versions, promotes them sequentially, and triggers rollback_champion() to verify that the active pointer returns to the previous healthy version.
  4. Audit the JSON log: Inspect the generated registry.json file to confirm that all promotion and rollback events are chronologically recorded with timestamps, approver names, and reasons.