AI system design: components, trade-offs and failure modes

Every serious decision in an AI architecture is a decision about where the boundary sits between what the model decides and what the system decides.

A reference for designing production AI systems: the components, what each one decides, how each one fails, and the trade-offs. The worked example throughout is a customer support system for a global retail bank.

Watch the walkthrough More resources

The whole system at a glance

The worked example, assembled. Purple boxes are system-controlled components while magenta is where judgement is delegated to a model. Note that learned models appear on both sides; for example an embedding or a reranker is a model inside a controlled pipeline. The question is not whether a component contains ML, but whether we are relying on model judgement for something we need to guarantee. Everything along the bottom runs across every path.

Customer API gateway Authentication · rate limiting · validation Identity and authorisation Who this is · what they own · what they may do AI orchestrator Intent classification Proposes a category Knowledge Metadata filters Hybrid retrieval Reranker Data and actions Tool selection Validate and permit Risk tier and confirm Escalation Handoff with context Fraud · vulnerability disputes · uncertainty Document index Synced from source Banking services The source of truth Support agents People Model layer Model gateway Routing · version pinning · fallback Small - classification Mid - routine support Strong - complex reasoning Output checks PII · escaping · policy Conversation state Never a source of truth Across every path Audit logging Observability Evaluation Safety controls Reliability Untrusted content handling, circuit breakers, retries and idempotency sit here too

The Data Layer

The same architecture, viewed by what it stores and what it depends on.

There are three bands pictured below: the synchronous request path across the top, which every customer message travels through under a latency budget; the data ingestion lane and the external dependencies below it, neither of which runs inside a request; and the four stores along the bottom.

Each store has a different consistency guarantee, a different lifetime and different behaviour when it fails. Treating them as one undifferentiated database is how a stale index gets mistaken for a source of truth, and how a cache quietly starts serving something it shouldn't.

Synchronous request path Latency-budgeted. Streaming responses hold the connection open, so this tier is long-lived rather than request-response. ASYNCHRONOUS · OWN WORKERS · OWN SCALING Nothing in this lane runs inside a customer request. Source systems Change events Queue absorbs bursts Workers parse, chunk Embed then index NOT YOURS TO OPTIMISE Rate-limited, independently operated. Core banking services The source of truth. Their latency is your floor. Model providers Quotas, versions and outages - hence the gateway and fallbacks. FOUR STORES, FOUR CONSISTENCY STORIES Vector index Eventually consistent by design Rebuilt continuously from source Staleness SLA: five minutes If it is down Serve static FAQ content only Conversation store Short-lived, scoped to a session Never a source of truth Re-query rather than reuse If it is down Context lost, no data lost Cache tiers Keyed by data sensitivity Public facts long, balances never Every tier is a staleness trade If it is down Slower and dearer, still correct Audit log Append-only and tamper-evident Retention governed independently Regulatory, legal and privacy duties If it is down Stop writes. Don’t proceed unlogged Queues, retries with backoff, circuit breakers and load shedding live between these tiers - the AI layer doesn’t replace any of it. Application services stay stateless and scale horizontally. Everything a request depends on lives in one of the four stores above.

Establish these before you draw anything

EstablishWhy it changes the designIn the bank example
Which kinds of request existKnowledge, private data and state-changing actions need entirely different machinery. Sorting them is the first architectural act, and everything downstream follows from it.Policy questions, balance lookups, and freezing a card
What is authoritative for eachRetrieval is approximate by design. It finds things that are similar, which is the wrong property for anything with one correct answer.Documents for fees, the core banking system for balances, never the index for both
Freshness tolerance per sourceDifferent sources tolerate different staleness, and that decides ingestion strategy and caching.Policy within minutes, balances in real time
Request amplificationUser traffic and model traffic are not equal. One message can become a routing call, embeddings, a rerank, several tool calls and a generation. Your architecture sets the ratio.10k concurrent users is nowhere near 10k model calls

Trace a request

One message does not mean one model call. Choose a request and a set of design decisions, then step through what actually happens and what it costs.

Latencies are an illustrative budget, not measurements.

Request
Router
Retrieval
Orchestration
0operations
0model calls
0 mselapsed

    The Components

    Each sits on the side of the boundary that owns its decision. System-controlled components enforce what the application has to guarantee. Model-judgement components interpret or generate, where some uncertainty is tolerable. The question is not whether a model is involved but whether a model is being asked to decide something that needs a guarantee.

    Select a component below for details on what it decides, how it fails, and what it looks like in the worked example.

    Request type

    What happens under load

    Capacity planning follows the execution graph, not user concurrency because user traffic and model traffic are different numbers. Every tier below can be the one that gives way first, and each has a different correct response. Scale what you own; protect what you don’t.

    Limit pointYours to scale?What to do when it binds
    Edge rate limitsYoursFirst line of admission control. Cheapest place to say no, and the only one that protects everything behind it.
    Application concurrencyYoursStateless services scale horizontally. Cap in-flight requests per instance so queueing is explicit rather than emergent.
    Queue and admission controlYoursAbsorb bursts, shed load deliberately when the queue grows past what the latency budget allows. A fast honest refusal beats a thirty-second hang.
    Model provider quotasTheirsNot scalable on demand. Route cheaper work to smaller models, degrade optional calls first; reranking before generation, never authorisation.
    Tool and API concurrencySharedConnection pools and per-tool concurrency caps, so one busy tool can’t starve the rest.
    Downstream banking systemsTheirsThe one you most need to protect. Circuit breakers and backpressure upstream, so your traffic spike doesn’t become their incident.

    The order matters. Shedding at the edge costs one refused request; shedding at the banking API costs everyone already mid-conversation.

    Decisions you should be able to justify

    Naming components is the easy half. These are the forks where a design is actually judged.

    DecisionOptionsWhat tips it
    How to routeRules, a trained classifier, embedding similarity, or a small modelRules add negligible cost and latency and are fully deterministic, but they get brittle as language varies. A classifier is cheap and scales. A model is flexible and costs you latency on every request. Most real systems are hybrid: deterministic for obvious intents, model for ambiguous free text.
    One agent or severalSingle orchestrator with many tools, or separate domain agentsSplitting buys focused prompts, smaller tool sets and separate ownership. It costs model calls, latency, orchestration and much harder debugging. Split when domain separation gives real operational benefit.
    Keeping knowledge currentScheduled reprocessing, or event-driven on source changeScheduled is trivial to build and your staleness window is your timer. Event-driven reacts to change and costs you a pipeline. Anything with a tight SLA needs the second.
    Retrieval strategyVector only, keyword only, or hybrid with a rerankerEmbeddings handle meaning and are weak on exact strings like product names, codes, policy numbers. Where exact terminology matters, hybrid plus reranking is worth the extra hop.
    Model allocationOne strong model everywhere, or tiered by taskOne model is simpler to operate and reason about. Tiering cuts cost and latency substantially but multiplies the surface you have to evaluate. Classification almost never needs a frontier model.
    Provider failureEvaluated fallback, degrade, or fail closedA fallback you have never evaluated is an untested deployment that fires on your worst day. For high-stakes paths, refusing is often safer than silently switching to different behaviour.
    What to rememberSession only, or persistent per-customer memoryPersistence buys personalisation and takes on consent, retention, deletion and residency. In regulated settings the default should be small, explainable and deletable.
    CachingNone, exact-match, or semanticCache by data sensitivity rather than by regeneration cost. Semantic caching needs real care wherever two similar-looking questions have different correct answers.

    What happens when something breaks

    Each of these should have an answer before launch, not during an incident.

    FailureResponseWhat the customer sees
    Model provider is downFail over to an evaluated alternative; if none exists for that task, degradeSlower, or a narrower set of things it can help with
    Vector index unavailableServe static FAQ content; suppress anything that needs retrievalCommon answers still work, specific policy questions don't
    Banking API unavailableRefuse to answer from memory or cacheLive account information is temporarily unavailable - never a guessed number
    Tool times outReturn an explicit failure to the model rather than silenceA clear "couldn't complete that", not a hallucinated confirmation
    A dependency fails repeatedlyCircuit breaker opens, fail fast, recover deliberatelyFast honest failure instead of a thirty-second hang
    Knowledge base is staleQuery the source directly for time-sensitive fields; alert on the staleness SLANothing
    Retrieved document contains instructionsTreat retrieved content as untrusted; authorisation and tool limits hold regardlessPossibly a worse answer. Never a wider permission
    Latency jumpsRead the distributed trace, find the stage, fix that stageDepends how fast you read the trace
    Traffic spikesScale stateless services, protect downstream dependencies, shed load before the banking API feels itQueuing, then graceful degradation, not a cascade

    Tool risk tiers

    Reading a balance and moving money are not the same risk and shouldn't carry the same controls. Before any tool execution, it's useful to ask what has to be true before it's allowed to start.

    TierExamplesControls before it runs
    ReadBalance, transactions, card status, loan detailsAuthenticated identity, permission check, access logged
    Low-risk writeUpdate contact preference, flag a transaction for reviewAll of the above, plus argument validation and an idempotency key
    High-risk writeFreeze a card, dispute a charge, move moneyAll of the above, plus a trusted confirmation the model cannot satisfy on the customer’s behalf. Step-up authentication, transaction signing or a human operator all qualify. In this design it is an application control outside the conversation, so the model can neither generate it nor read a reply as consent

    Where the time goes

    A “two-second response” is two numbers, not one. Perceived latency is how long until something useful appears on screen; completion latency is how long until the task is finished. Streaming reduces percieved latency but does nothing to completion latency.

    The budgets below are illustrative not measured. Capturing latency budgets makes the architectural cost visible early. Six sequential model calls, for example, loses either budget before any code is written.

    StageBudgetNote
    Gateway and identity~60 msFixed cost on every request
    Routing~40 msNegligible on rules, real on a small model
    Retrieval~80 msCan run in parallel with a data lookup
    Reranking~50 msSkippable for high-confidence single-hit queries
    Banking API~150 msNot yours to optimise
    Generation - first token~300–600 msEverything above plus this is what the customer waits on
    Generation - remainder~0.8–1.4 sStreams while they read. Counts against completion, not perception

    On these figures, perceived latency lands near 600 ms to a second, and completion nearer 1.5 to 3 seconds.

    For actions, assistant latency and business-operation latency can also be viewed differently: a dispute or a payment can legitimately take far longer than the answer describing it, and the assistant should say so rather than hold a connection open.

    What to measure

    A service can return a 200, in under a second, with a fluent answer that's wrong. Every monitor green and the system broken. So evaluation runs per layer, not just at the end.

    LayerMeasuresThe question it answers
    RetrievalRecall@K, MRR, NDCGDid the right evidence come back, and was it near the top?
    GenerationGroundedness, faithfulness, relevance, citation accuracyIs the answer actually supported by what we retrieved?
    Tool useSelection accuracy, argument accuracy, task completion, unnecessary callsRight tool, right arguments, and nothing it didn't need to touch?
    RoutingIntent accuracy, false positives onto privileged routes, escalation recallDid it send this to the right place, and does it over-reach?
    SafetyInjection resistance, unauthorised access attempts, PII leakage, policy complianceDoes the boundary hold when someone pushes on it?
    End to endTask success, containment, escalation rate, recontact rateDid the customer get what they came for?
    OperationsLatency, availability, error rate, throughput, costThe ordinary questions, which still apply
    OutcomeCost per successfully resolved taskA cheap model that makes people ask three times isn't cheap

    Averages hide the failures that matter

    Every metric above needs slicing, because there might be an imbalance. The cases a system handles worst are almost never the cases it sees most.

    Illustrative figures. The pattern is the point, not the numbers.
    SliceTool selection accuracyWhy it diverges
    Overall98.7%Dominated by high-volume, low-stakes requests
    Card freeze99.9%Narrow phrasing, heavily represented in the suite
    Disputed transaction94.1%Ambiguous language, overlapping tools, far rarer
    Vulnerable customer flows90.8%Least represented, highest consequence, hardest to get right

    The headline figure is fine but the system is not. Slice by request type, by risk tier, by language, by channel, and by the customer circumstances you have a duty of care toward, then set a threshold per slice rather than one number for everything.

    An invitation

    Shared in the spirit of contributing.

    This resource is shared to do three things: pass on what I’ve learned, invite scrutiny from others doing the same work, and contribute to the wider conversation about what production-grade actually means for these systems.

    It takes positions rather than listing options. Where the boundary sits between system and model. What to degrade first under load. When to fail closed. If you’d draw one of those lines differently, or you’ve run a system where one of them broke, that’s the feedback I most want. Reach me through the contact form or on LinkedIn.

    Free to use and adapt with attribution — CC BY-SA 4.0.