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:

  1. 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.

  2. 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.

  3. 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.

  4. 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".

  5. 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.

Loading diagram...

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

  1. Do populate context with the required category-specific keys before calling validate() — for MODEL_SELECTION ADRs, validate() checks that {"model_name", "provider", "use_case"} is a subset of context.keys() and raises ValueError if any are missing; supplying structured metadata here is what makes model decisions queryable and diff-able rather than free-text blobs.
  2. Do ensure criteria_weights values sum to 1.0 within the 0.01 tolerance enforced by validate() — the method computes sum(criteria_weights.values()) and rejects any total outside [0.99, 1.01]; a weights dict that silently drifts (e.g., two criteria at 0.6 each) will block the record from ever reaching ACCEPTED status and forces the decision rationale to stay explicit.
  3. Do set a review_by datetime and bind telemetry_validators to real production checksis_expired() always returns False when review_by is None, so governance dashboards can never surface stale decisions; without both fields populated, a MODEL_SELECTION or RAG_VS_FINE_TUNING ADR that the evidence has turned against will keep its ACCEPTED status indefinitely instead of triggering a supersession.

Don'ts

  1. Don't express a new decision type as a free-text entry in context instead of extending DecisionCategory — every category that isn't a DecisionCategory enum member (e.g., HOSTING_STRATEGY, EMBEDDING_MODEL) sits outside the typed taxonomy, so it can't be filtered by category, depended on via dependencies, or superseded programmatically — reproducing exactly the unqueryable free-text blobs the schema exists to eliminate.
  2. Don't leave dependencies: list[str] empty when the ADR's outcome builds on an upstream decision — the field holds adr_id strings that link downstream ADRs to the records they depend on; omitting it means a RAG_VS_FINE_TUNING ADR that rests on a specific EMBEDDING_MODEL or HOSTING_STRATEGY choice has no machine-readable link, so superseding the upstream record leaves orphaned, invalid decisions silently active.
  3. Don't skip populating consequences to avoid a ValueError in validate()validate() explicitly raises if self.consequences is empty, and the field is the textual contract that telemetry_validators verify post-deployment; an ADR without consequences has no stated predictions to measure, making every entry in telemetry_validators unanchored 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.