Free lesson
Build ADR schema and decision taxonomy for GenAI technology choices
You will build an `ADRSchema` with a structured decision taxonomy covering GenAI-specific technology choices. Define a `DecisionCategory` enum with variants: `MODEL_SELECTION`, `HOSTING_STRATEGY`, `RAG_VS_FINETUNING`, `GUARDRAIL_PLACEMENT`, `EMBEDDING_PROVIDER`, `VECTOR_STORE_CHOICE`. Implement `ArchitectureDecisionRecord` as a Pydantic model with fields: `adr_id: str`, `title: str`, `category: DecisionCategory`, `status: ADRStatus` (PROPOSED, ACCEPTED, SUPERSEDED, DEPRECATED), `context: str`, `decision: str`, `consequences: list[str]`, `created_at: datetime`, `superseded_by: Optional[str]`, `decision_makers: list[str]`, `tags: list[str]`, `review_deadline: Optional[datetime]`, `priority: int`. Build `DecisionOption` models with `option_name: str`, `provider: str`, `scores: dict[str, float]` mapping criteria like `latency_score`, `cost_score`, `quality_score`, `security_score` to 0-1 normalized values, and `evidence: list[str]` containing URLs or benchmark references supporting each score assignment. Implement `WeightedCriteriaMatrix` that accepts a `criteria_weights: dict[str, float]` and computes `compute_weighted_score()` returning a ranked `ScoredOption` list with `total_score: float` and `rank: int` fields. Build `validate_weights()` ensuring all weights sum to 1.0 within floating-point tolerance, raising `InvalidWeightsError` with details if they do not. Store ADRs in PostgreSQL `architecture_decisions` table with columns: `id SERIAL PRIMARY KEY`, `adr_id VARCHAR(64) UNIQUE NOT NULL`, `title TEXT NOT NULL`, `category VARCHAR(32) NOT NULL`, `status VARCHAR(20) NOT NULL DEFAULT 'PROPOSED'`, `context TEXT`, `decision TEXT`, `consequences JSONB`, `created_at TIMESTAMPTZ DEFAULT NOW()`, `updated_at TIMESTAMPTZ`, `superseded_by VARCHAR(64) REFERENCES architecture_decisions(adr_id)`. Create a `decision_options` junction table with `option_id SERIAL PRIMARY KEY`, `adr_id VARCHAR(64) REFERENCES architecture_decisions(adr_id)`, `option_name VARCHAR(128)`, `provider VARCHAR(64)`, `scores JSONB`, `evidence JSONB`. Build `create_adr()` FastAPI endpoint at `POST /api/v1/adrs` accepting an `ADRCreateRequest` body with required fields `title`, `category`, `context`, `decision`, and returning `ADRCreateResponse` with the generated `adr_id` and `created_at` timestamp. Implement `list_adrs()` at `GET /api/v1/adrs?category={cat}&status={status}&page={page}` with pagination and filtering support. Implement `get_adr_history()` at `GET /api/v1/adrs/{adr_id}/history` that returns the full version chain including all supersede links as a list of `ADRVersionEntry` objects with `adr_id`, `status`, `created_at`, `superseded_by`. Deploy `adr_versions_total{category,status}` Prometheus counter tracking ADR creation rates, `adr_options_evaluated_total{category}` counter tracking scoring activity, and `adr_creation_latency_seconds` histogram measuring endpoint performance. Build `supersede_adr()` at `POST /api/v1/adrs/{adr_id}/supersede` that atomically marks the old ADR as SUPERSEDED and links the new one via the `superseded_by` foreign key, enforcing referential integrity in a PostgreSQL transaction with `SELECT FOR UPDATE` to prevent concurrent supersede race conditions.
~25 min read · Free to read — no subscription required.
Design a typed ADR schema that captures GenAI-specific decision categories including model selection, hosting strategy, and RAG-vs-fine-tuning trade-offs
Introduction
When you copy a traditional ADR template into a GenAI codebase, the fields stop fitting almost immediately — there is no slot for token pricing curves, no field for context-window limits, no place to declare which production telemetry should invalidate the decision later. Teams that ship GenAI systems on legacy ADR shapes end up with free-text "decision" blobs that resist querying, dependency tracking, and automated review, so superseded model choices linger in production long after the evidence has turned against them. By the end of this lesson you will be able to design a typed ADR schema whose category enum, structured context, criteria-weight matrix, and dependency links make every GenAI architecture decision machine-readable, auditable, and reviewable against live production signals.
Key Terminology
- Architecture Decision Record (ADR): A structured, persisted document that captures an architecturally significant decision — its context, the option chosen, alternatives considered, and downstream consequences — so future reviewers can audit why the system looks the way it does.
- Decision Taxonomy: The controlled vocabulary of categories (e.g. MODEL_SELECTION, HOSTING_STRATEGY, RAG_VS_FINE_TUNING) that classifies every ADR. Encoding it as an enum lets the schema enforce category-specific required fields and makes governance queries type-safe.
- Telemetry Validator: A named production check (e.g. "p95_latency_under_500ms") bound to an ADR at creation time. When the check fails against live metrics, the governance system flags the decision as stale without needing human review.
Concepts
The two ideas below justify the schema's shape: first, why GenAI architecture work needs its own category taxonomy rather than reusing a generic ADR template, and second, the design principles — explicit criteria weights, bound telemetry validators, enum-typed status, and dependency edges — that the ADRSchema must encode to stay auditable in production.
Why GenAI Decisions Demand a Custom Taxonomy
A decision taxonomy is a controlled vocabulary that classifies every architecture decision into a well-defined category. In GenAI systems, three decision categories dominate day-to-day architectural work, and each carries distinct evaluation criteria:
- Model Selection: Which foundation model serves a given capability? This decision involves cost-per-token, latency percentiles, quality benchmarks (MMLU, HumanEval, domain-specific evals), context window size, fine-tuning support, and vendor lock-in risk.
- Hosting Strategy: Where does inference run? Options span fully-managed APIs (OpenAI, Anthropic, Google), cloud-hosted endpoints (SageMaker, Vertex AI, Azure ML), and self-hosted infrastructure (vLLM on dedicated GPUs). Each choice trades off cost control against operational burden.
- RAG vs. Fine-Tuning: How does the system incorporate domain knowledge? Retrieval-Augmented Generation keeps the base model frozen and injects context at query time; fine-tuning bakes knowledge into model weights. Hybrid approaches combine both. The decision depends on data freshness requirements, training infrastructure availability, and acceptable latency budgets.
Beyond these three primary categories, a production taxonomy also needs categories for guardrail strategy (content filtering approach), embedding model selection (vector representation choices), and orchestration pattern (single-call vs. agent loop vs. chain-of-thought pipeline). The schema must be extensible to accommodate these without breaking existing records.
Design Principles for Production ADR Schemas
Several principles emerge from this schema design that apply broadly to any ADR system targeting GenAI workloads:
-
Enforce category-specific invariants at creation time rather than at query time. A malformed ADR that enters the system will pollute dependency graphs, skew governance dashboards, and resist cleanup. The validate method and build_category_context factory catch problems before persistence.
-
Make criteria weights explicit and normalized. When the weighted criteria matrix is embedded directly in the ADR record, any reviewer can see exactly how cost, latency, and quality were balanced. Requiring weights to sum to 1.0 prevents the common mistake of adding a new criterion without rebalancing existing ones.
-
Bind telemetry validators to decisions at creation time. The telemetry_validators field names the specific production checks (e.g., "p95_latency_under_500ms", "cost_per_request_under_0.02") that validate this decision's assumptions. When validators are declared upfront, the telemetry validation system can automatically detect stale decisions without human intervention.
-
Model status transitions as an enum, not a string. Using ADRStatus instead of free-text status values prevents typos, enables exhaustive pattern matching in governance logic, and makes status-based queries type-safe. A decision is either ACCEPTED or it is not—there is no ambiguity from variations like "approved", "active", or "in_effect".
-
Design for dependency traversal from day one. The dependencies field stores ADR IDs as a list of strings, creating edges in a decision dependency graph. When a hosting strategy decision is superseded, every downstream model selection ADR that assumed that hosting environment must be flagged for review. Without explicit dependency tracking, these cascading invalidations go unnoticed until production incidents surface them.
The schema you have built in this section forms the structural backbone for everything that follows: the weighted scoring engine in another goal, the telemetry validation pipeline in another goal, the dependency graph traversal in another goal, and the governance dashboard in another goal. Every downstream system consumes ADRSchema instances and relies on the invariants enforced by validate and build_category_context to guarantee data quality.
Code Walkthrough
Building on the decision taxonomy and design principles above, the following Python translates them into concrete types: the DecisionCategory and ADRStatus enums, and the ADRSchema dataclass with its validate and is_expired methods. The foundation of a typed ADR system is an enum that exhaustively lists every decision category your organization recognizes, paired with a dataclass that defines the shape of every ADR record. ADRSchema enforces that each record carries a category drawn from the taxonomy, a structured context dictionary for category-specific metadata, a criteria_weights mapping for the weighted criteria matrix, dependencies that link this decision to upstream ADRs, and telemetry_validators that bind it to production checks.
Code snippetpython
1from dataclasses import dataclass, field 2from enum import Enum 3from datetime import datetime 4from typing import Optional 5import uuid 6 7class DecisionCategory(Enum): 8 MODEL_SELECTION = "model_selection" 9 HOSTING_STRATEGY = "hosting_strategy" 10 RAG_VS_FINE_TUNING = "rag_vs_fine_tuning" 11 GUARDRAIL_STRATEGY = "guardrail_strategy" 12 EMBEDDING_MODEL = "embedding_model" 13 ORCHESTRATION_PATTERN = "orchestration_pattern" 14 15class ADRStatus(Enum): 16 PROPOSED = "proposed" 17 ACCEPTED = "accepted" 18 SUPERSEDED = "superseded" 19 DEPRECATED = "deprecated" 20 REJECTED = "rejected" 21 22@dataclass 23class ADRSchema: 24 title: str 25 category: DecisionCategory 26 context: dict 27 decision: str 28 consequences: list[str] 29 criteria_weights: dict[str, float] = field(default_factory=dict) 30 dependencies: list[str] = field(default_factory=list) 31 alternatives_considered: list[dict] = field(default_factory=list) 32 status: ADRStatus = ADRStatus.PROPOSED 33 adr_id: str = field(default_factory=lambda: f"ADR-{uuid.uuid4().hex[:8]}") 34 created_at: datetime = field(default_factory=datetime.utcnow) 35 review_by: Optional[datetime] = None 36 superseded_by: Optional[str] = None 37 telemetry_validators: list[str] = field(default_factory=list) 38 39 def validate(self) -> bool: 40 if not self.title or not self.decision: 41 raise ValueError("ADR must have both title and decision text") 42 if not self.consequences: 43 raise ValueError("ADR must document at least one consequence") 44 if self.category == DecisionCategory.MODEL_SELECTION: 45 required = {"model_name", "provider", "use_case"} 46 if not required.issubset(self.context.keys()): 47 raise ValueError(f"Model selection ADR requires context keys: {required}") 48 if self.criteria_weights: 49 total = sum(self.criteria_weights.values()) 50 if abs(total - 1.0) > 0.01: 51 raise ValueError(f"Criteria weights must sum to 1.0, got {total}") 52 return True 53 54 def is_expired(self) -> bool: 55 if self.review_by is None: 56 return False 57 return datetime.utcnow() > self.review_by
The DecisionCategory enum lists the three primary GenAI categories plus three extension categories production teams commonly need, each storing a snake_case value for serialization. ADRStatus models the full lifecycle, from PROPOSED through ACCEPTED, SUPERSEDED, DEPRECATED, or REJECTED. The ADRSchema dataclass declares every field a record carries, including the criteria_weights decision matrix and the telemetry_validators that run post-deployment. The validate method enforces structural invariants — non-empty title and decision, at least one consequence, required context keys for model-selection ADRs, and criteria weights summing to 1.0 within tolerance — raising a ValueError on any violation. The is_expired method returns False when no review_by deadline is set and otherwise compares the current UTC time against it, letting governance dashboards surface stale decisions. Verify by instantiating a MODEL_SELECTION ADR with context keys model_name, provider, and use_case and criteria_weights summing to 1.0, then calling validate — you'll know it works when it returns True, while an instance missing those keys or with weights off by more than 0.01 raises ValueError.
Do's and Don'ts
Do's
- ✓Do populate
contextwith the required category-specific keys before callingvalidate()— forMODEL_SELECTIONADRs,validate()checks that{"model_name", "provider", "use_case"}is a subset ofcontext.keys()and raisesValueErrorif any are missing; supplying structured metadata here is what makes model decisions queryable and diff-able rather than free-text blobs. - ✓Do ensure
criteria_weightsvalues sum to 1.0 within the 0.01 tolerance enforced byvalidate()— the method computessum(criteria_weights.values())and rejects any total outside[0.99, 1.01]; a weights dict that silently drifts (e.g., two criteria at0.6each) will block the record from ever reachingACCEPTEDstatus and forces the decision rationale to stay explicit. - ✓Do set a
review_bydatetime and bindtelemetry_validatorsto real production checks —is_expired()always returnsFalsewhenreview_byisNone, so governance dashboards can never surface stale decisions; without both fields populated, aMODEL_SELECTIONorRAG_VS_FINE_TUNINGADR that the evidence has turned against will keep itsACCEPTEDstatus indefinitely instead of triggering a supersession.
Don'ts
- ✗Don't express a new decision type as a free-text entry in
contextinstead of extendingDecisionCategory— every category that isn't aDecisionCategoryenum member (e.g.,HOSTING_STRATEGY,EMBEDDING_MODEL) sits outside the typed taxonomy, so it can't be filtered by category, depended on viadependencies, or superseded programmatically — reproducing exactly the unqueryable free-text blobs the schema exists to eliminate. - ✗Don't leave
dependencies: list[str]empty when the ADR's outcome builds on an upstream decision — the field holdsadr_idstrings that link downstream ADRs to the records they depend on; omitting it means aRAG_VS_FINE_TUNINGADR that rests on a specificEMBEDDING_MODELorHOSTING_STRATEGYchoice has no machine-readable link, so superseding the upstream record leaves orphaned, invalid decisions silently active. - ✗Don't skip populating
consequencesto avoid aValueErrorinvalidate()—validate()explicitly raises ifself.consequencesis empty, and the field is the textual contract thattelemetry_validatorsverify post-deployment; an ADR without consequences has no stated predictions to measure, making every entry intelemetry_validatorsunanchored and the review cycle meaningless.
Keep going with GenAI Solutions Architecture
Create a free account to track your progress and open this lesson in the full learning view. Subscribe to unlock the entire path — every goal, the hands-on labs, quizzes, and your verifiable skill graph — from . Cancel anytime.