Production AI System Design
By the end: You choose one decision from your own log, list the assumptions it rests on, name the strongest alternative and the measurable triggers that would reopen it, and answer one challenge by checking the log rather than arguing.
At a glance
Show the design as a chain of decisions, link each decision to dated assumptions, and let written triggers, not the argument, decide when to reopen it.
A chain, not a model
Decision, data, training, serving, gate, monitoring, cost; GenAI adds retrieval and a trust boundary.
Two linked logs
Assumption register and decision records, joined by the assumptions each decision lists.
Queues from the log
Failed assumption: reopen. Date passed: review. No assumption: untestable.
Triggers
Metric, threshold, window, owner and the action they fire.
Six moves
Decision, constraints, strongest alternative, weakest assumption, failure and rollback, triggers.
Recall
From memory: (1) Name three differences between batch, online and shadow serving. (2) Which signal fires first when inputs drift, and which one arrives last? (3) Which control stopped the injected passage in your Week 11 incident? Then clear your due review cards.
Concept
At a senior level, system design is a chain of dependent decisions, not a model choice. Every link rests on assumptions: how often the consumer acts, how fast a feature changes, how a vendor behaves, whether held-out queries look like real ones. Defending a design means showing which assumption would break which link, and not defending it past that point.
Predict first
Raising one assumed input moves the point at which the cheapest option at your quality floor flips. What do you record?
Text description of the figure
A cost, latency and quality lab with editable, hypothetical price, routing share, cache hit rate and context length inputs. Changing an input moves the cheapest option that meets a quality floor, so the lab shows which assumption the choice depends on.
| Criterion | Prediction servicecard-fraud scoring (banking) | GenAI servicepolicy assistant (insurance) |
|---|---|---|
| Decision and consumer | Approve, decline or step up each card authorisation | Claims handler quoting cover to a policyholder |
| Data | Transactions with a data contract and point-in-time features | Policy wordings with versions, effective dates and permissions |
| Model step | Trained model against the current rules | Retrieval, context assembly, generation mode |
| Serving | Online under a latency budget; shadow first (Day 19) | Per request, with an untrusted-input boundary (Day 53) |
| Evaluation gate | Time-ordered backtest in losses and false declines | Golden set with slices, attack suite (Days 51, 55) |
| Monitoring and rollback | Drift, skew, chargeback labels weeks late; previous model one switch away | Abstention rate, citation share, incidents; previous index or prompt |
| Cost unit | Compute per 1,000 scored transactions | Tokens per request and per good answer (Day 54) |
Each cell rests on assumptions. The defence names the one most likely to be false.
The toy decision log and assumption register of the application (synthetic), audited on 2026-10-11 by the same rules as its code. Components are generic; no real organisation is described.
Audit on 2026-10-11: D1 review, D2 reopen, D3 ok, D4 untestable. Dashed lines are the loops that move assumptions: decisions change the data the next model learns from, and monitoring fires the fallback.
Each badge is a decision from the toy log, coloured by the audit the application code runs. The dashed lines are where assumptions move.
- Assumptionsconfidence, owner, datelisted by
- Decision recordslist their assumptions
- audit: to Assumption status?
none listed - Untestablenothing can ever fail
- Assumption status?
- date passed: to Review
- low confidence: to Watch
failed - Reopencompete again on evidence
- Reviewvalidate before relying
- Watchlow confidence
Each decision lists the assumptions it rests on. Failed: reopen. Revisit date passed: review. Low confidence: watch.
| Decision | Rests on | Flag | Next step |
|---|---|---|---|
| D1 Batch no-show scores, 4 runs a day | A1, A2 | Review: A2 was due 2026-10-01 | Validate A2 before relying on D1 |
| D2 Hosted embeddings for retrieval | A3, A4 | Reopen: A3 (latency) failed | Compare with the fallback on current evidence |
| D3 BM25 as fallback retriever | A4 | ok | Survives the event that reopens D2 |
| D4 Retrain monthly on a calendar | none | Untestable | Record what the calendar assumes |
Decision and consumer
What is decided, by whom, how often.
Constraints and success
Latency, cost, quality floor; the number that defines success.
Strongest alternative
Why it lost, with the evidence and its limits.
Weakest assumption
The one most likely to be false and how you would find out.
Failure and rollback
What breaks first and how you get back.
Triggers
Metric, threshold, window, owner: when the decision reopens.
| Part | Example | Missing it means |
|---|---|---|
| Metric | p95 latency of the embedding call | Nobody knows what to watch |
| Threshold | 800 ms | Every wobble is an argument |
| Window | seven consecutive days | It fires on noise |
| Owner | on-call engineer for retrieval | It never fires |
| Action | route to the fallback, open a superseding record | It fires and nothing happens |
- The Assumption register (capability S19): statement, evidence, confidence, owner, impact if wrong, how to validate, revisit date. Revise entries; never overwrite them.
- The Decision record (ADR) (S05, extending Nygard's short format of context, decision and consequences): context, options with evidence, decision, consequences, a measurable Revisit trigger.
- The link: each decision lists its assumptions. A failed assumption reopens its decisions, a passed date queues a review, a decision with none is untestable.
- Six moves fill six minutes: decision and consumer; constraints and success criterion; strongest alternative; weakest assumption; failure mode and rollback; triggers.
- Under challenge, check the register, apply the trigger, and supersede with a new record if the evidence moved. Scenarios 008, 011 and 012 practise this.
Assumptions
You kept some entries in the Decision log and Assumption register in Weeks 1-11; entries you write today are labelled "recorded late" with today's date.
Numbers in a decision come from your own runs or from inputs labelled hypothetical; neither is presented as a measurement of a real organisation.
A decision is defended against the evidence available now; evidence that arrives later may legitimately change it.
Application and workplace decision
- Pick one decision from your Decision log, or write one now marked "recorded late".
- (a) List its two to four assumptions with confidence, owner and revisit date.
- (b) Write the strongest alternative and the evidence for and against it.
- (c) Write the triggers as metric, threshold, window and owner.
- (d) Say the six moves aloud against a timer, then answer from the register: "your key assumption failed; what do you do?"
- Then run the code: it audits a toy log of four decisions and four assumptions and prints what to reopen, review, watch or distrust.
Python
from datetime import date
TODAY = date(2026, 10, 11)
# Toy log (synthetic). Assumption id -> (statement, confidence, revisit date, status)
assumptions = {
"A1": ("Revenue-management run happens 4 times a day", "high", date(2026, 12, 1), "holds"),
"A2": ("The strongest feature changes slower than 6 hours", "medium", date(2026, 10, 1), "holds"),
"A3": ("Hosted embedding API p95 latency stays under 800 ms", "low", date(2026, 11, 1), "failed"),
"A4": ("Held-out queries match how agents phrase questions", "medium", date(2026, 11, 15), "holds"),
}
# Decision id -> (title, assumption ids it rests on)
decisions = {
"D1": ("Serve no-show scores in batch, 4 runs a day", ["A1", "A2"]),
"D2": ("Use hosted embeddings for retrieval", ["A3", "A4"]),
"D3": ("Keep BM25 as the fallback retriever", ["A4"]),
"D4": ("Retrain monthly on a fixed calendar", []),
}
reopen = []
for did, (title, ids) in decisions.items():
flags = ["UNTESTABLE: no recorded assumption"] if not ids else []
for a in ids:
_, confidence, due, status = assumptions[a]
if status == "failed":
flags.append(f"REOPEN: {a} failed")
reopen.append(did)
elif due <= TODAY:
flags.append(f"REVIEW: {a} was due {due}")
elif confidence == "low":
flags.append(f"WATCH: {a} has low confidence")
print(f"{did} {title}: {'; '.join(flags) or 'ok'}")
print("decisions to reopen:", reopen)
Mini-assessment
Technical depth
The method: availability of a chain of componentsSeries and parallel availability derived from the multiplication rule, a four-link service worked by hand, and where independence breaks.
- share of time a component works; 99.9% over 30 days (43,200 minutes) means 43.2 minutes down
- mean time between failures and mean time to repair: faster repair raises A as much as rarer failure
- series: a request needs every link, so the probabilities that each works multiply; the chain is worse than its worst link
- parallel: the step works if any copy works, so the probabilities that each fails multiply
- both products assume components fail independently; a shared cluster, region or vendor breaks that
Derived from the multiplication rule for independent events. The request path of an ML or GenAI service is a series chain; a fallback is a parallel branch.
- API gateway99.95%
- on failure: to BM25 fallback
embed - Embedding APIhosted; 99.5%: the weakest link
- search: to Vector index
- BM25 fallback99.9%: a parallel branch
- passages: to LLM API
- Vector index99.9%context
- LLM API99.7%generate
- Answer99.05% in series; 99.65% with fallback
A request needs every link, so availabilities multiply: 409 minutes down per 30 days in series, 151 with the fallback around retrieval. The same arithmetic covers a card-authorisation path, a telecom fault-detection pipeline or a hospital triage-score service; log each link's assumed availability.
| Part of the request path | Availability | Down per 30 days | How it is computed |
|---|---|---|---|
| API gateway | 99.95% | 21.6 min | 0.0005 x 43,200 |
| Hosted embedding API | 99.5% | 216 min | 0.005 x 43,200: the weakest link |
| Vector index | 99.9% | 43.2 min | 0.001 x 43,200 |
| LLM API | 99.7% | 129.6 min | 0.003 x 43,200 |
| Whole chain in series | 99.05% | 409 min (6.8 h) | 0.9995 x 0.995 x 0.999 x 0.997 |
| Retrieval with a BM25 fallback (99.9%) in parallel | 99.9994% | 0.3 min | 1 - (1 - 0.995 x 0.999)(1 - 0.999) |
| Whole chain with the fallback | 99.65% | 151 min | 0.9995 x 0.999994 x 0.997: now the LLM API dominates |
| Fallback on the vector index's own cluster | 99.55% | 194 min | one cluster outage takes both branches: retrieval = 0.999 |
Redundancy on the weakest link cut downtime from 409 to 151 minutes, and the binding constraint moved to the next link. A fallback that shares a dependency with what it backs up adds less than the formula promises. Recomputed in Python.
- Whole chain in series409 min
- BM25 fallback around retrievalindependent: retrieval 99.9994%−258 min
- Fallback on the index's clusterone outage takes both branches+43 min
- Shared-cluster fallback194 min
Downtime per 30 days from the worked table. The independent fallback's 151 minutes is the running total after the first step; now the LLM API dominates.
Error budgets: deciding in advance how much failure is acceptableThe budget and burn-rate formulas, the multi-window alert arithmetic, and how a burn-rate alert is a pre-registered trigger.
- service-level objective: the share of good events promised, e.g. 99.9% of requests answered under the latency limit
- error budget: the bad events allowed in the window T; 0.1% of 30 days is 43.2 minutes, or 1 in 1,000 requests
- burn rate: how many times faster than allowed the budget is being spent; r = 1 spends exactly B over T
- the alert window as a share of the SLO window: one hour of a 720-hour month is 1/720
A budget turns "is this bad enough to act?" into a number agreed in advance, the same discipline as a written reconsideration trigger in the decision log.
| Alert | Burn rate and window | Budget used when it fires | Budget gone at that rate in |
|---|---|---|---|
| Page | 14.4x over 1 hour | 14.4 x 1 / 720 = 2% | 720 / 14.4 = 50 hours |
| Page | 6x over 6 hours | 6 x 6 / 720 = 5% | 120 hours (5 days) |
| Ticket | 1x over 3 days | 1 x 72 / 720 = 10% | 30 days: on track to just meet the SLO |
Each alert also requires a short confirmation window of about 1/12 of its long window to exceed the same rate, so it stops firing soon after recovery. Example values from the Google SRE workbook; the arithmetic is the method.
The budget
1 - 0.999 = 0.1% of authorisation calls may be slow over 30 days (720 hours).
One bad hour
1.44% of calls were slow in the last hour.
Burn rate
1.44 / 0.1 = 14.4 times faster than allowed.
Budget used
14.4 x 1 / 720 = 2% of the month's budget in one hour: the page fires.
| Criterion | Long window only | Short window only | Both required |
|---|---|---|---|
| A one-minute blip | Ignored | Pages someone on noise | Ignored |
| After the service recovers | Keeps firing for hours | Stops quickly | Stops within the short window |
The same trade-off as the trigger window in the decision log, with the numbers made explicit.
- Budget left this window?
- yes: to Ship model changes
no: spent - Reliability work firstuntil the burn rate recovers
- Ship model changesthe risk is affordable
A decision rule agreed before the incident, not during it.
Little's law: why latency triggers are capacity assumptionsadvancedLittle's law, the single-queue latency formulas, a worked utilisation table, a sizing example and a simulation that checks both.
- mean requests in the system = arrival rate x mean time in system; any stable system, any distributions, long-run averages
- service rate of one server or replica (requests per second it can finish)
- utilisation: the share of time the server is busy; must stay below 1 or the queue grows without bound
- M/M/1 (Poisson arrivals, exponential service, one FIFO server): latency depends on the spare capacity, not on load alone
- time in an M/M/1 system is exponential with rate mu - lambda, so its 95th percentile is ln 20 / (mu - lambda), about 3 / (mu - lambda)
Little's law is an identity about averages; use it for sizing. The M/M/1 formulas are a model of the latency curve's shape: several replicas, batching, long-tailed prompts and retries (extra arrivals exactly when the system is slow) push the real tail above them, while Little's law still holds.
| Arrivals per second | Utilisation | Mean time in system | In system: lambda x W | p95 time in system |
|---|---|---|---|---|
| 5 | 0.50 | 1 / 5 = 0.20 s | 1.0 | 3.00 / 5 = 0.60 s |
| 7 | 0.70 | 1 / 3 = 0.33 s | 2.3 | 1.00 s |
| 8 | 0.80 | 1 / 2 = 0.50 s | 4.0 | 1.50 s |
| 9 | 0.90 | 1 / 1 = 1.00 s | 9.0 | 3.00 s |
| 9.5 | 0.95 | 1 / 0.5 = 2.00 s | 19.0 | 5.99 s |
Check: rho / (1 - rho) gives the same L as lambda x W in every row. From 80% to 90% busy the mean doubles. A p95 under 800 ms needs mu - lambda above ln 20 / 0.8 = 3.74, so arrivals under 6.26 per second: utilisation below 63%.
- mean: simulated (dots), 1/(μ − λ) (line)
- p95: simulated (squares), ln 20/(μ − λ) (line)
Simulated mean time in system: 0.50 s at 80% busy, 1.01 s at 90% (formula 0.50 and 1.00). With this service rate a p95 of 800 ms only holds below about 63% utilisation.
At 90% busy the arrivals found 9.1 requests in the system on average; λ x W gives 9.1. Little's law needs no distributional assumption, only a stable system.
Seeded simulation of one server with Poisson arrivals and exponential service times (an M/M/1 queue), 40,000 requests per utilisation after a warm-up of 2,000. Real services have several replicas and heavier tails; the shape, not the numbers, carries over.
Left, simulated mean and p95 latency against the queue formulas as utilisation rises. Right, Little's law checked on the same simulated runs.
Little's law
12 requests per second x 2.5 s in system = 30 requests in flight.
At 100% busy
Replicas with 4 concurrent slots: 30 / 4 = 7.5 replicas, with no headroom at all.
At a 70% target
30 / (4 x 0.7) = 10.7, rounded up to 11 replicas.
Register the inputs
Arrival rate and time in system are dated assumptions; a new traffic forecast reruns this arithmetic.
- Traffic forecast λan assumption with a datedivide by μ
- Utilisation ρ = λ / μμ = 10 requests/s per replica
- M/M/1: to p95 = ln 20 / (μ − λ)
- Reopen the decisionbefore users feel it
- p95 = ln 20 / (μ − λ)the M/M/1 shapecompare
- p95 above 800 ms?
- yes: back to Reopen the decision
no - Keep; date the check
Register both assumptions, with the utilisation at which the latency one fails (63% for one replica finishing 10 requests per second), so a traffic forecast can reopen the decision before users feel it.
Answering a challenge with the triggerThe decision path when someone says your key assumption is failing, and how the window length trades false alarms for delay.
- Written trigger met?
- yes: to Act and supersede
no - Evidence nearing it?
- yes: to Update register
no - Keep and note it
- Act and supersedefallback now; new dated record
- Update registerlower confidence, earlier datethen
- Prepare cheaplyshadow-test the fallback
The trigger was agreed before the data arrived. Evidence that approaches it changes confidence, dates and preparation, not the criterion.
A 1-day window fires 15 times before anything changed. A 7-day window never fires on noise and holds 16 days after the real shift.
- false firings before the shift (count)
- days from the shift until the trigger holds
The window is a dial between two costs: arguments and cut-overs on noise, or days of a real breach before anyone acts. Choose it from the cost of each, then write it down before the data arrive.
Seeded synthetic series: 240 days, level 720 ms then 860 ms from day 170, autocorrelated noise. Shows the trade-off, not any vendor's behaviour.
Same threshold, ten window lengths, computed on one synthetic latency series with a real shift.
What to register, and why it driftsAssumptions worth tracking and the ML-specific debts that move them.
Under several decisions
One failure reopens many; monitor it first.
Cheap to check
A query or a dashboard settles it; no reason not to.
Expensive if wrong
Serving mode, vendor, data contract: costly to reverse.
Feedback loops
Once a model changes what is observed next, retraining data describe its own effect.
Entanglement
Changing one input changes everything downstream of it.
Hidden feedback loops
Model outputs shape the next training data.
Configuration debt
Thresholds and settings scattered without owners.
Categories from Sculley et al. (2015). Each is a reason to register and date the assumption it threatens.
Hidden feedback loopsadvancedHow a model's own policy corrupts its next training set, and the fixes that must exist before the loop starts.
5 of 8 buckets reminded.
2 of 8 buckets reminded.
4 of 8 buckets reminded.
Retraining on its own outcomes, the realised no-show rate swings by 5.0 points between rounds (9.9% to 14.9%). Retraining on a never-reminded random holdout keeps it near 12.6%.
Seeded simulation: 8 risk buckets, 300 customers each per round, a reminder at learned risk 0.15 or above, and an assumed reminder effect of 50%; the fix keeps 30% of each bucket as a never-reminded holdout. Bars: learned risk (orange = reminded); black ticks: true risk; dashed: the threshold.
Reminders lower no-shows, as for clinic appointments or service bookings: the model's own policy changes the outcomes it is retrained on. Nothing about the customers changes between rounds.
| Criterion | Untouched random holdoutthe policy never acts on it | Log action and its probabilitywith every prediction | Action as a model input | Assumption with a trigger |
|---|---|---|---|---|
| What it costs | Some value lost on the holdout | Logging and storage discipline | Needs variation in who received it | One register entry |
| What it buys | Unbiased outcomes to retrain on and to measure the policy effect | Off-policy estimates of what another policy would have done | Separates risk from the effect of the intervention | Someone notices when the training data start describing the model |
The first two need to exist before the loop starts; outcomes not logged under a known policy cannot be recovered later.
The same chain for a GenAI serviceOne testable assumption per link, from corpus freshness to price.
- CorpusDocuments are current; versions and permissions are tracked.
- QueriesHeld-out queries look like how agents actually ask.
- RetrievalThe embedding provider's latency and model stay within bounds.
- GenerationThe pinned model version exists until its announced retirement.
- CostPrices, cache hit rate and escalation share stay near the measured values.
Write each one as a statement a number could contradict, with a validation method and a date.
Which decisions deserve the full treatmentCost to reverse against uncertainty, and where a design review should spend its time.
Experiment
Try it behind a flag and measure; a short note is enough.
Full record and triggers
Serving mode, vendor, data contract: assumptions, alternatives, a measured fallback.
Just decide
Library minor versions, plot styles; no record.
Record once
Expensive but well understood: write the decision and revisit on a date.
Practitioner heuristic. The top-right cell is where a design review spends its six minutes: serving mode, vendor, data contract, evaluation design. A removed component gets a record too, with the trigger that would bring it back.
Superseding a recordHow a decision changes without rewriting history.
Keep the old record
Status becomes superseded, with a link to its successor.
State the new evidence
What moved, when, measured how.
Compare again
The same alternatives on current evidence, plus any new one.
New triggers
Fresh metric, threshold, window and owner; a better criterion applies from now on.
What a staff-level defence addsadvancedBlast radius, a priced reversal path, named loops, a pre-mortem and what was deliberately not built.
Blast radius
"A2 sits under two decisions, so it is monitored first."
Reversal path priced
"Falling back costs the measured quality gap; staying costs p95 above 800 ms."
Loops named
"The collections model decides who gets a call; the next model learns from that, so we keep a holdout."
Pre-mortem
"If this fails in six months, the likeliest cause is the vendor latency assumption."
What we chose not to build
"No online scoring yet; the trigger that would bring it back is written down."
Memory anchor
Defend the process, not the verdict: every decision names its assumptions and the number that would reopen it.
Useful for
Preparing a design review, an architecture-board presentation or a senior system-design interview.
Turning a pile of project notes into a log that tells the next owner when to reopen a choice.
Deciding which assumptions deserve monitoring because they sit under several decisions.
Not suitable for
Replacing evaluation; a well-kept log records that a decision was made on evidence but does not create the evidence.
Small, reversible choices; a full record for every library version produces noise and hides the decisions that matter.
Traps
Defending the decision instead of the process
Under challenge you argue that the original choice was right and ignore that its assumption has moved, so the reviewer concludes you cannot revise.
How to avoid it
Open the register, state what changed, apply the trigger and supersede the record if the evidence says so.
Assumptions that cannot be tested
"Users will like it" or "traffic will be moderate" sit in the log with no metric, owner or date, so nothing can ever mark them failed.
How to avoid it
Write each assumption as a statement a number could contradict, with a validation method and a date.
Senior reasoning
A senior designs for reversal. They keep a measured fallback so one failed assumption does not take down the system, they write triggers as metric, threshold, window and owner, and they treat a review challenge as a free validation of the weakest assumption. They also prune: a log of every choice is as useless as no log, so they record the decisions that cost most to reverse.
Interview question
"Describe an architecture decision you made, what you gave up, and what would make you reverse it." How would you structure the answer?
Recall cards scheduled after completion
- What are the elements of an assumption register entry and of a decision record?
- How do the two logs tell you what to reopen?
Prerequisites and related concepts
References
- Alerting on SLOs (Site Reliability Workbook, chapter 5)optional
Burn rate relative to the SLO and the multiwindow, multi-burn-rate alert example (14.4x over 1 hour, 6x over 6 hours, 1x over 3 days) used in the error-budget chapter.
- Hidden Technical Debt in Machine Learning Systemscore
ML-specific debt such as entanglement, hidden feedback loops and configuration debt explains why assumptions about data and consumers drift and must be registered.
- The ML Test Score: A Rubric for ML Production Readiness and Technical Debt Reductioncore
A production-readiness rubric of tests for data, model, infrastructure and monitoring that doubles as a checklist for the evaluation and monitoring links in the chain.
- Documenting Architecture Decisionsoptional
The short decision-record format with status, context, decision and consequences that the decision log follows.
- Artificial Intelligence Risk Management Framework (AI RMF 1.0)optional
Govern, map, measure and manage vocabulary for documenting AI risks and ownership in a design.
- Designing Machine Learning Systemsoptional
End-to-end production ML design, including deployment, distribution shift and monitoring chapters.
Optional sections
Deep dive
Writing a trigger. A usable trigger names four things: the metric (p95 latency of the embedding call), the threshold (800 ms), the window (seven consecutive days) and the owner (the on-call engineer for retrieval). Add the action it fires (route to fallback and open a superseding record). Triggers without a window fire on noise; triggers without an owner never fire.
Which assumptions to register. Prefer assumptions that sit under several decisions (A2 in question q-058-2), that are cheap to check, or that are expensive to be wrong about. For learned systems add the hidden feedback loop: once a model's output changes what is observed next, the data it retrains on describe the model's own effect.
The same chain for a GenAI service. Decision and consumer, corpus and its manifest, retrieval and its held-out evaluation, context assembly, untrusted-input boundary, generation mode with an evidence label, per-request cost and latency, monitoring (abstention rate, citation share, incidents), rollback to the previous index or prompt version. Each link has its own assumptions: corpus freshness, query distribution, provider behaviour and price.
Interview practice
You are told in review that your design is over-engineered for a team of two. How do you respond?