Back to home
Interview Prep

Ask the right question before picking an answer

A working reference for system design judgment — the clarifying questions, approach options, and trade-offs behind data, communication, compute, and ML decisions, read through the same SWE Compass lens as the rest of this site.

On this page
Before The Questions

A working reference, not interview trivia

Navigating ambiguity and asking the right clarifying question before reaching for an implementation isn't an interview trick — it's the actual skill, valuable on the job independent of whoever's in the room, across a system's full lifecycle: design, build, operate, evolve.

The SWE Compass — three axes from one origin — is the lens this page reads through:

  • End-to-end — why the questions below are grouped into lifecycle-adjacent categories (data, communication, compute, ML) rather than one long undifferentiated list.
  • Abstraction— why each question's cascade climbs from a general principle down through concrete pattern options to a collapsed layer of implementation and vendor specifics.
  • Time— why questions cross-link via "Also affects" ripples, and why scaling-progression and the ML deployment risk ladder exist as their own entries rather than a footnote.

Grounding this in something more than assertion: roadmap.sh/software-architect frames architecture as ongoing judgment — decision-making under ambiguity as a named skill, documentation as a continuously-maintained artifact rather than a one-time deliverable, and architecture judged by long-term maintainability, not just greenfield design. That's the frame this whole page is built on.

North Star
"Navigate ambiguity by understanding what to use, when to use it, and why to use it."

— Thomas To

Applying the North Star — three questions, in order

  1. 1

    What are you trying to do?

    The outcome the business needs and who it serves — scope and a definition of "done", stated before anything technical.

    Unlocks: A bounded problem the why can be measured against.

  2. 2

    Why are you doing it?

    The motivation and the success criteria — which characteristics matter most, and which trade-offs are acceptable to get them.

    Unlocks: The constraints that rule approaches in or out.

  3. 3

    How are you going to do it?

    The technical approach — components, data flow, and trade-offs — chosen only once the what and the why are settled, and judged against them.

    Unlocks: A design you can defend, not a preference.

The order is the point. Whether the data leads you toward a data-intensive pipeline or a customer-facing frontend, the fundamentals don't change — the what bounds the why, and the why judges the how. Worked examples live in Communication Scripts and throughout the page.

End-to-enddesign → opsAbstractionhardware → distributedTimeiteration
Foundation

The core, and what builds on it

Every requirement a system has splits into two kinds. Functional is what the system should do — its features, behaviors, API, endpoints. Non-functional is how well it should do that — performance, scalability, reliability, and so on. Everything below is non-functional: none of it says what gets built, only the bar it has to clear once it exists.

The first 7 are the baseline every design decision on this page gets checked against. 4 are Chip Huyen's canonical characteristics of a well-designed system, from Designing Machine Learning Systems(O'Reilly, 2022). The other 3 are CAP theorem's triad — during a network partition, a distributed system can only fully guarantee two of the three. A further 6 — latency, throughput, bandwidth, durability, security, cost — round out the non-functional picture below; they're common enough to be worth knowing cold, even without carrying that same baseline weight.

This site's own Projects page uses a close variant — reliability, maintainability, adaptability, and resiliencein place of scalability — a deliberate adaptation for a system running on a $0 budget that can't vertically scale.

Chip Huyen's 4

Reliability

Keeps working correctly even when components fail — degrade gracefully, don’t go down.

Scalability

Handles growth in load, data, or complexity without a fundamental rewrite.

Maintainability

A different engineer — or you, in a year — can understand, fix, and extend this without archaeology.

Adaptability

Absorbs new requirements, new data patterns, or new constraints without a ground-up redesign.

CAP's 3

Consistency

Every read gets the latest write — no node ever serves stale data.

Availability

Every request gets a response, even if the data behind it isn’t the latest.

Partition Tolerance

The system keeps working despite dropped or delayed messages between nodes.

Other Non-Functional Characteristics

Latency

How long one request takes, start to finish — the user-facing clock.

Throughput

How many requests or operations the system completes per unit of time — its overall capacity under load.

Bandwidth

How much raw data the network can carry per unit of time — the size of the pipe, not any single request’s speed through it.

Durability

Once written, data survives crashes and restarts — it doesn’t just respond, it persists.

Security

Only the right users and services can access, change, or act on the system’s data.

Cost

What it takes in infrastructure and engineering time to build and run this — the constraint every other trade-off gets weighed against.

Once the first 7 hold, everything else is situational — which characteristic matters most, and how much, depends on this system's specific constraints:

Self-Assessment

A 5-dimension scoring rubric

Communication

  • 1 — Needs work: Silent or hard to follow — the interviewer can’t tell what you’re thinking.
  • 3 — Solid: Explains decisions when asked, but doesn’t volunteer the reasoning first.
  • 5 — Staff-level: Narrates trade-offs proactively, without being prompted.

Say the trade-off out loud before the interviewer asks "why" — silence reads as uncertainty even when you're not.

Breadth

  • 1 — Needs work: Fixates on one component while the rest of the system goes unaddressed.
  • 3 — Solid: Covers the expected major components at a reasonable level.
  • 5 — Staff-level: Proactively surfaces components the interviewer didn’t hint at.

Sketch the whole system at a high level before diving deep anywhere — breadth first, depth second.

Depth

  • 1 — Needs work: Names components at a surface level only — no detail behind the label.
  • 3 — Solid: Can go one level deeper when asked to.
  • 5 — Staff-level: Anticipates the deep-dive and pre-empts it before being asked.

For the 1-2 components you expect the interviewer to probe, rehearse the next level down before the interview.

Trade-off Articulation

  • 1 — Needs work: Presents one option as the only option.
  • 3 — Solid: Names alternatives when asked.
  • 5 — Staff-level: Proactively names 2-3 alternatives with a reasoned pick.

Use the phrasing "I'm choosing X over Y because..." even when nobody asked — it signals you know the alternatives exist.

Proactive Problem Identification

  • 1 — Needs work: Needs the interviewer to point out every bottleneck.
  • 3 — Solid: Catches bottlenecks once the interviewer flags them.
  • 5 — Staff-level: Identifies bottlenecks before being asked.

After sketching a component, ask yourself out loud "what breaks this at 10x traffic?" before moving on.

What separates staff-level from passing

  • A passing answer covers the expected components; a staff-level answer covers them and names which ones matter most, and why
  • A passing answer explains a trade-off when asked; a staff-level answer volunteers 2-3 alternatives and defends the pick unprompted
  • A passing answer treats the design as finished at the end; a staff-level answer names what it would revisit with more time or more information
  • A passing answer reasons about the happy path; a staff-level answer reasons about failure modes and operational concerns from the start
  • A passing answer picks a reasonable database; a staff-level answer states the specific, measurable threshold that would change the pick

Cross-referenced against Hello Interview's own 4 evaluation dimensions

Problem Navigation
Maps closest to Breadth and Proactive Problem Identification above — can you scope and steer the conversation, not just answer questions.
Solution Design
Maps closest to Depth and Trade-off Articulation — the substance of the architecture itself, not just how it’s presented.
Technical Excellence
Cuts across Depth and Trade-off Articulation — the correctness and sophistication of the specific technical choices made.
Communication and Collaboration
Maps directly to Communication above — narrating reasoning and treating the interviewer as a collaborator, not an examiner.
Say This, Not Nothing

6 reusable communication scripts

Opening

When to use: Right after requirements gathering, before touching the whiteboard — restate the problem and your plan.

So to make sure I have this right: we're building [system], and the core requirements are [1-2 functional requirements] with [non-functional requirement, quantified]. I'd like to start by sketching the high-level components, then go deep on [the part most likely to matter]. Does that match what you're looking for?

Proposing a component

When to use: When introducing a new piece of the architecture for the first time.

I'm going to add a [component] here, whose job is to [one-sentence responsibility]. It sits between [upstream component] and [downstream component], and the reason I'm introducing it now rather than later is [reason].

Articulating a bottleneck

When to use: When you spot a scaling or performance concern, especially before the interviewer flags it.

One thing I want to flag before moving on: [component] is going to become a bottleneck once [condition, ideally quantified] — I'd address that by [lever from the scaling-progression ladder], but I want to note it now rather than come back to it later.

Mentioning a trade-off

When to use: Any time you pick one approach over a real alternative — say this even if not asked.

I'm choosing [option X] over [option Y] here. The trade-off is that [X] gives us [benefit], at the cost of [downside] — given [requirement/constraint], that downside is acceptable.

"Why not X?"

When to use: Answering a direct challenge on an alternative you didn't pick — confidently, not defensively.

Good question — [X] was a real option. I didn't go with it because [specific reason tied to a requirement], but if [condition changed], I'd reconsider it. It's not that [X] is wrong, it's that it optimizes for something we don't need as much here.

Wrap-up

When to use: In the last few minutes — summarize the design and show it isn't presented as finished.

To summarize: we designed [system] with [2-3 core components] handling [core flow], using [key decision] to satisfy [requirement]. If I had more time, I'd want to go deeper on [known gap] and pressure-test [assumption] — those are the parts I'm least certain about right now.

Foundations

The fundamentals underneath every design

Three layers, each built on the one before: how an engineer approaches a problem, how an architect turns that into trade-offs between named characteristics, and the lifecycle data moves through before any model or dashboard can trust it. The rest of this page assumes all three.

1 · Software engineering

From coder to engineer

The habits that separate shipping code from engineering a system — each one shows up in a design interview as a question you should be asking, or a signal you should be giving.

Engineering is more than codeThe job is solving a business problem under constraints — code is one of the tools.

The jump from programmer to engineer is the jump from "does it work?" to "does it solve the right problem, at acceptable cost, in a way others can maintain?" In an interview, that is the difference between naming a technology and justifying one.

Ch. 1

Read and understand before you changeMost engineering happens inside systems you did not write — reading code is a core skill.

Real systems are brownfield. Tracing how an existing codebase behaves, finding its seams, and changing it safely matters more than greenfield speed. In a design interview, the analog is asking what already exists before proposing a rewrite.

Ch. 2, 6

Model before you buildDiagrams and models are how a design gets communicated and challenged before it is expensive to change.

A model is a cheap, disposable version of the system that exposes misunderstandings early. The whiteboard in a system design interview is exactly this — a shared model, not a decoration.

Ch. 4

Let drivers shape the architectureRequirements, constraints, and quality attributes drive architecture — not fashion.

An architecture is justified by its drivers: the functional requirements, the constraints you cannot change (budget, team, compliance), and the quality attributes that matter most. State the drivers out loud before naming any component.

Ch. 9

Automated tests are the safety net for changeA trustworthy automated test suite is what makes a system safe to evolve.

Maintainability is not a property of clean code alone — it is the confidence to change code without breaking it. In design terms: how will this be tested, at which layer, and what would catch a regression before users do?

Ch. 5

Data outlives the code around itData models and storage choices are among the hardest decisions to reverse.

Application code gets rewritten; data and its schema tend to persist and accumulate dependents. Choosing a storage engine and data model deserves more deliberation than choosing a framework — the data engineering lifecycle below goes deeper.

Ch. 8

Reliable, repeatable path to productionSoftware creates no value until it is deployed — and deployment should be boring.

Automated, repeatable deployment (CI/CD, environment parity, rollbacks) turns releases from events into routine. In an interview, mentioning how a design gets deployed and rolled back is a cheap, strong operability signal.

Ch. 10

Choose tools by fit, not trendEvaluate options against the problem and its constraints before reaching for the newest thing.

Every tool brings operational cost, a learning curve, and failure modes. "We already run Postgres and it fits" is often a stronger answer than a specialized store — as long as the trade-off is named.

From the book's stated themes

Communication is an engineering skillA design nobody understands, or nobody agreed to, does not get built well.

Explaining trade-offs, writing decisions down, and bringing people along are part of the engineering, not an add-on to it. The communication scripts above are this, made concrete for an interview.

Ch. 13

2 · Software architecture

Trade-offs, not best practices

Core Characteristics names the qualities a system can have; this is how an architect decides which ones matter, proves the system has them, and picks a shape that can deliver them.

Everything in software architecture is a trade-off.

There is no "best practice" answer to an architecture question — only the least-bad option for these drivers. If an option seems to have no downside, the downside has not been found yet.

Why is more important than how.

Anyone can read a diagram to see how a system is built; the reasoning behind it is what gets lost. Say why out loud in an interview, and write it down (as a decision record) in real life.

From characteristic to governed system

  1. 1Architectural thinkingBreadth over depth: knowing many options well enough to compare them beats knowing one deeply.

    An architect’s value is in the trade-off analysis between options, which requires technical breadth. It also means staying close enough to the code to keep decisions grounded.

    Ch. 2
  2. 2Modularity: cohesion and couplingHow well a system splits into parts — high cohesion inside a module, low coupling between them.

    Modularity is the raw material every architecture style is built from. Coupling is the cost of change: the more modules must change together, the less independently they can be deployed, scaled, or owned.

    Ch. 3
  3. 3Identify the characteristics that matterDerive the few characteristics that matter from requirements and business context — not all of them.

    Every characteristic added makes the design more complex, so the goal is the smallest set that captures what the business actually needs. Domain concerns ("time to market", "user satisfaction") translate into characteristics (agility, performance, availability).

  4. 4Measure and govern with fitness functionsMake characteristics objectively measurable, then check them automatically so they don’t drift.

    A characteristic nobody measures is an aspiration. Fitness functions — automated checks such as latency budgets in CI, dependency rules, or chaos tests — turn "it should be scalable" into something a build can fail on.

    Ch. 6
  5. 5Scope characteristics to the right part of the systemDifferent parts of a system often need different characteristics — which is what drives where to split it.

    If checkout needs high availability and reporting needs high throughput, forcing both onto one deployment unit means paying for both everywhere. Independently deployable units with their own characteristics (the book’s architecture quantum) are the real reason to split a system.

    Ch. 7
  6. 6Component-based thinkingPartition components by technical layer or by business domain — and know which you chose.

    Technical partitioning (presentation / business / persistence) is familiar but spreads every feature across layers. Domain partitioning (orders, payments, catalog) keeps a feature’s change in one place. That top-level choice constrains which architecture styles fit.

    Ch. 8
  7. 7Record architectural decisionsWrite decisions down with their context and consequences, so the "why" survives.

    A lightweight decision record — context, decision, consequences, status — is the practical form of the second law. In an interview, narrating a decision in that shape is a strong staff-level signal.

  8. 8Analyze architecture riskAssess risk per characteristic and per area of the system, then address the highest first.

    Risk is likelihood × impact for each characteristic in each part of the architecture. Doing it collaboratively surfaces risks no single person sees — and gives the deep-dive portion of an interview a principled target.

9 architecture styles, and when to reach for eachEvery style is a pre-packaged set of trade-offs — pick the one whose strengths match the characteristics you identified.

LayeredTechnical partitioning · Ch. 10

Reach for it when: Small, simple apps or a fast start with a team that knows the pattern.

Watch out for: Every feature change cuts across all layers; scales and deploys as one unit.

Modular monolithDomain partitioning · Ch. 11

Reach for it when: You want domain boundaries without distributed-system cost — often the right first step.

Watch out for: Module boundaries erode without enforcement (fitness functions help).

PipelineTechnical partitioning · Ch. 12

Reach for it when: One-directional processing — ETL, compilers, data transformation flows.

Watch out for: Poor fit for interactive, request/response workloads.

MicrokernelTechnical partitioning · Ch. 13

Reach for it when: A stable core plus customer- or feature-specific plug-ins (IDEs, rules engines).

Watch out for: The plug-in contract becomes hard to change once many plug-ins depend on it.

Service-basedDomain partitioning · Ch. 14

Reach for it when: A pragmatic middle ground: a handful of coarse domain services, often sharing a database.

Watch out for: Shared database couples services’ schemas and release cycles.

Event-drivenTechnical partitioning · Ch. 15

Reach for it when: High responsiveness and scale with asynchronous, loosely coupled processing.

Watch out for: Harder to test, debug, and reason about consistency and error handling.

Space-basedTechnical partitioning · Ch. 16

Reach for it when: Extreme, spiky concurrency (ticketing, auctions) where the database is the bottleneck.

Watch out for: In-memory data grids are complex and costly; eventual persistence to the database.

Orchestration-driven SOATechnical partitioning · Ch. 17

Reach for it when: Mostly a historical lesson in enterprise-wide reuse via a central orchestration bus.

Watch out for: Reuse-driven coupling makes change slow — the problem microservices react against.

MicroservicesDomain partitioning · Ch. 18

Reach for it when: Independent deployability and scaling per domain, with teams aligned to services.

Watch out for: Distributed-systems cost: network latency, data consistency, operational overhead.

3 · Data engineering

The lifecycle data moves through before anyone can trust it

Five stages turn data produced somewhere else into something analytics, models, and operational tools can rely on — and six undercurrents apply at every one of them. The Data and Model questions further down assume this lifecycle already exists.

Data engineering lifecycle

Adapted from Reis & Housley, Fundamentals of Data Engineering (O'Reilly, 2022). Select any box to jump to its explanation.

Stages · Ch. 5–9

GenerationData is produced at the source — application events, third-party APIs, user input.

Source systems are usually owned by someone else — an application team, a vendor, a device fleet. The data engineer consumes them, so the first job is understanding their shape and failure modes rather than designing them.

Questions to ask

  • What kind of source is it — an OLTP database, an event stream, a third-party API, logs, IoT devices?
  • How fast is data produced, and in what volume?
  • What is the schema, and how — and how often — does it change?
  • Who owns the source, and what happens downstream when it breaks or changes without notice?

Ch. 5

StorageLanded somewhere durable and queryable, at whatever tier matches its access pattern.

Storage is not a step data passes through once — it sits underneath ingestion, transformation, and serving, which all read from and write to it. That is why the figure draws it as a foundation layer rather than a box in the row.

Questions to ask

  • What read/write throughput and latency does each consumer need?
  • Will it scale to the expected volume, and at what cost per GB and per query?
  • Is the data hot (queried constantly) or cold (kept for compliance or reprocessing)?
  • Does the storage engine fit how the data will actually be queried — row vs. columnar, object vs. block?

Ch. 6

IngestionMoved from source into the pipeline, in batch or as a stream.

Ingestion is where source-system unreliability meets pipeline expectations, so it is often the most fragile stage. The batch-vs-streaming choice is a cost/latency trade-off, not a default.

Questions to ask

  • Batch or streaming — and does the consumer actually need real-time, or just "fresh enough"?
  • Push (the source sends) or pull (the pipeline fetches)?
  • What volume and frequency, and what happens to downstream jobs when a load is late or partial?
  • How are duplicates, late-arriving records, and schema drift handled?

Ch. 7

TransformationCleaned, joined, and reshaped into the form downstream consumers actually need.

Transformation is where raw data picks up business meaning — type casting, cleaning, joins, aggregation, and data modeling. Every transform has a compute cost, so it should be justified by a consumer that needs it.

Questions to ask

  • What business logic is being applied, and who owns its definition?
  • Which data model fits the consumers — normalized, dimensional (star schema), wide/denormalized?
  • Does it run in batch or in-stream, and how is it backfilled when logic changes?
  • Is the cost of computing it justified by the value downstream?

Ch. 8

ServingMade available to whatever consumes it next — a dashboard, an application, or a model.

Serving is where data finally produces value, and where trust is won or lost: a fast dashboard with wrong numbers is worse than no dashboard. The three classic consumers are analytics, machine learning, and reverse ETL.

Questions to ask

  • Who consumes it, through what interface, and with what freshness/latency expectation?
  • Do consumers trust it — are definitions consistent and is quality monitored?
  • Is access controlled to the right granularity for each consumer?

Ch. 9

Undercurrents — continuous, across every stage

Security

Protect data and access at every stage, not as a final gate.

  • Least privilege for people and service accounts
  • Encryption in transit and at rest
  • Security as culture, not just tooling — most breaches are human

Data management

Make data discoverable, trustworthy, and compliant across its whole life.

  • Governance, data quality checks, and ownership
  • Metadata and lineage — where data came from and what touched it
  • Privacy, retention, and regulatory compliance

DataOps

Apply DevOps and Agile habits to data: automate, observe, respond.

  • Automated deployment and testing of pipelines
  • Monitoring and observability of data health, not just job success
  • Incident response when bad data ships

Data architecture

Design the data system to serve business needs, and keep decisions reversible.

  • Trade cost against flexibility explicitly
  • Prefer reversible decisions; isolate the irreversible ones
  • Revisit the architecture as needs change

Orchestration

Coordinate jobs as dependency graphs with scheduling, retries, and alerting.

  • Model pipelines as DAGs of dependent tasks
  • Retries, timeouts, and backfills as first-class features
  • Alert on missed SLAs, not only on failures

Software engineering

Treat pipelines as software: versioned, tested, reviewed code.

  • Pipelines and infrastructure as code
  • Code quality, testing, and review apply to SQL and DAGs too
  • Build vs. adopt open-source vs. buy managed, decided deliberately

Downstream — what Serving feeds

Analytics

Business intelligence, operational dashboards, and analytics embedded in customer-facing products.

Machine learning

Features and training data for models — the Data stage the ML framework above assumes is already built.

Reverse ETL

Processed data pushed back into operational tools (CRM, ad platforms) — closing the loop to source systems.

The Framework

A repeatable 4-step approach

This is the tool you reach for during steps 2 and 3 below — the question bank in "Ask the right question before picking an answer" is what fills in the substance of the High-Level Design and Deep Dive steps.

Intro ~5 minScope ~5 minHigh-Level Design ~12 minDeep Dive ~13 minWrap-up ~5 minQ&A ~5 min

Worked for a typical 45-minute interview. Scope and Wrap-up hold at ~5 min regardless of length; High-Level Design and Deep Dive just split whatever time remains, evenly.

Intro ~5 min

Restate the problem in your own words, propose a rough agenda ("I'll spend a few minutes on scope, sketch the high-level design, then go deep on the part that matters most"), and confirm scope out loud — before the clock starts on the framework below.

  1. 1

    Requirements & Scope

    ~5 min

    Understand the problem before proposing anything. Separate functional requirements (what the system does) from non-functional ones (how well it does it).

    • Establish non-functional characteristics before functional ones — reliability, scalability, maintainability, adaptability, and the CAP trade-off (see Core Characteristics) shape every choice that follows, including what "done" even means here
    • Back-of-envelope the scale and performance targets early: expected QPS, data volume/growth, read:write ratio, and p99 latency — real numbers, not adjectives, decided before the first component gets drawn
    • Quantify non-functional requirements — "<500ms p99 latency" not "low latency", "99.9% availability" not "highly available"
    • Briefly name 2-3 core entities (the nouns the system revolves around) before moving to design
    • Confirm scope out loud — what is explicitly out of scope for this session matters as much as what is in scope
  2. 2

    High-Level Design

    Remaining time, split evenly with Deep Dive

    Sketch the major components and how data flows between them, at a level an interviewer can follow on a whiteboard.

    • Start with the simplest design that satisfies the requirements, then layer in complexity
    • Name each component’s responsibility in one sentence before drawing the next one
    • For at least one real fork (e.g. SQL vs NoSQL, monolith vs microservices), sketch 2 viable options out loud and name the trade-off that picked one — see the "Mentioning a trade-off" script
    • This is where the question bank below gets used — real-time vs batch, sync vs async, SQL vs NoSQL, each as they come up naturally
    • Check in with the interviewer before going deep on any one part — confirm the shape is right first
  3. 3

    Deep Dive

    Remaining time, split evenly with High-Level Design

    Pick the 1-2 components that matter most (either because the interviewer steers there, or because that’s where the interesting trade-offs live) and go deep.

    • This is where the implementation-notes ("Go deeper") layer of each question below gets used
    • Same discipline applies here: at least 2 options before committing, even for a single deep-dive component under time pressure
    • Proactively surface a bottleneck before being asked — see the Staff Signals section
    • Quantify: back-of-envelope numbers (requests/sec, storage growth) turn hand-waving into a real argument
  4. 4

    Wrap Up

    ~5 min

    Summarize the design, name its known weaknesses, and show awareness that the design continues evolving after the interview ends.

    • Restate the design in 2-3 sentences — shows you can hold the whole system in your head
    • Name what you’d do with more time, rather than presenting the design as finished
    • The interview isn’t over after the initial design — expect follow-up "what if" questions and treat them as more of the same process
Visualizing the Design

Which diagram, when

The visual companion to the framework above: a C4/container sketch and an ERD carry the High-Level Design step, while a sequence diagram or flowchart carries one specific interaction or decision path into the Deep Dive — each giving the interviewer something concrete to react to, and preparing the ground before you commit to going deep on one part.

C4 / Container diagram

When to use it: The macro shape — the system, its external dependencies, and who talks to whom — before any single component gets a deep dive. Usually the first thing drawn in a design interview.

This example: A typical container diagram: one client talking to a backend service, which checks a cache before reading/writing a primary datastore, delegates slow work to a background worker via a queue, and calls out to one or more external/third-party services.

UML sequence diagram

When to use it: One specific critical interaction, mapped in execution order — component lifelines, sync/async messages, and explicitly where failure/retry handling sits. Reach for this in the Deep Dive step, not the high-level sketch.

This example: A generic request flow: input validation, a cache check that falls through to the database on a miss, a downstream call to an external service, and an explicit error/retry branch when that external call is rate-limited or fails.

Entity-relationship diagram

When to use it: The data model — entities, keys, attributes, and how they relate. Don't force multiple entities where the real schema has one: an honest single-table ERD is a more accurate answer than an invented join.

This example: A single, deliberately denormalized entity — the right shape for a read-optimized system fed by a separate write/ingestion pipeline, where forcing a multi-table join would misrepresent how the data actually gets queried.

Flowchart / activity diagram

When to use it: Complex conditional logic inside one component — decision nodes, loops, terminal states. The right tool for an algorithm or an authorization hand-off, where a sequence diagram would be the wrong shape (no second participant to swim-lane against).

This example: A generic signed-access hand-off: a request carrying an access token gates a metadata lookup, which gates a fetch from private storage — with a distinct, specific error at every point the request can be rejected.

Q&A ~5 min

Treat this as more interview, not wind-down — the same "what if" follow-ups from Wrap Up often continue here. Ask something that shows genuine interest: the team's actual biggest pain point, or what a follow-up round would probe. Don't leave it blank — it's still a signal.

Do

  • Ask clarifying questions before proposing a solution — ambiguity is the point of the exercise, not an obstacle to rush past
  • Think out loud — narrate the trade-off you’re weighing, not just the conclusion
  • Propose multiple approaches when a real fork exists, then explain why you picked one
  • Identify the critical components first, before filling in every peripheral detail
  • Quantify wherever possible — a number beats an adjective

Don't

  • Don't jump straight to a solution before requirements are understood
  • Don't front-load excessive detail on a component before confirming the interviewer wants to go there
  • Don't think silently — a long pause with no narration reads as stuck, even when you're not
  • Don't assume the interview ends after the initial design — expect and welcome follow-up questions
  • Don't present one option as the only option when real alternatives exist
The ML-Specific Framework

How the framework adapts for a model in production

An ML system design interview runs on the same instincts as the 4-step framework above — clarify, design, go deep, wrap up — with one extra layer bolted on: a model that has to be trained, evaluated, and kept correct after it ships, not just deployed once and left alone.

  1. 1

    Requirements & Problem Framing

    ~5 min

    Translate a business problem into an ML task before proposing any architecture — what is actually being predicted, and for whom.

    • Clarify the business problem and its downstream use — the same product-scope clarifying questions as general system design, plus: what does the model's output actually get used for?
    • Identify the ML task type this reduces to — classification, regression, ranking, clustering — since that choice shapes every later step
    • Confirm scale and latency constraints the same way the general framework does — but also ask whether inference runs in the cloud or on-device, since on-device inference trades server latency for memory and battery constraints instead
  2. 2

    Metrics & Evaluation Criteria

    ~2-3 min

    Decide upfront how success will be measured, before any design gets proposed — there is no way to know a design worked without this.

    • Define both an offline metric (held-out data, gates the ship decision) and an online metric (live user behavior, proves it actually helped) — see the Offline vs. Online Metrics question
    • Debate the trade-offs between candidate metrics out loud: sensitivity to class imbalance, and the likely gap between the offline number and the online result
    • Metrics can be added or removed later in the design if a better one surfaces — this isn't a one-shot decision
  3. 3

    High-Level Architecture

    ~10 min

    Sketch a modular design — one logical block per ML component — rather than one monolithic model doing everything.

    • A cascade or tiered approach (cheap candidate generation/filtering, followed by a more expensive ranking stage) is the standard shape for retrieval-and-ranking problems — see the Quora and Uber Michelangelo case studies in Sources
    • Each block in the diagram represents one ML component with a clear responsibility — the same discipline as naming each component's job in the general framework's High-Level Design step
  4. 4

    Offline Model Building & Evaluation

    Remaining time, split with Deep Dive

    Work through data generation, feature engineering, and model training/evaluation — starting from the simplest baseline, not the most sophisticated model.

    • Think through multiple ways to generate data and labels: implicit signals (a logged purchase), explicit signals (a user 'saves' an item), human-labeled data, or some combination
    • Engineer features that leverage the product's unique properties, and consider feature crosses (combining features) where a single feature alone underspecifies the signal
    • Transform raw features into a usable format: scale numeric values (min-max normalization, z-score standardization, or log-scaling for skew), encode categoricals (one-hot for low-cardinality, embedding learning for high-cardinality IDs like user/item), and decide deletion vs. imputation (mean/median/mode) for missing values — each with a real trade-off, not a default
    • Start with a non-ML heuristic or a simple baseline (logistic regression) before reaching for something more complex (gradient-boosted trees/random forests, then neural networks) — debate data volume needed, training speed, compute/GPU requirements, and interpretability for each option, the same 'name at least two options' discipline as the general framework
  5. 5

    Deep Dive

    Remaining time, split with Offline Model Building

    Go deep on the 1-2 components you're most confident about — the data, modeling, and evaluation choices, and their known shortcomings — the same way the general framework's Deep Dive step works.

    • Surface real modeling risks unprompted: class imbalance, noisy labels, calibration — proactively, the same Staff Signals instinct as general system design
    • If time allows, propose concrete ways to improve the chosen component further
  6. 6

    Monitoring, Logging & Alerts

    Optional, if time allows

    A model that shipped correctly can still go wrong silently in production — monitoring and a retraining plan are part of the design, not an afterthought.

    • Identify concrete failure modes to watch for: miscalibrated predictions, online-metric degradation — see the Drift vs. Staleness question for how to tell a changed world from a stale model
    • Propose a mitigation plan for each failure mode, plus a fallback — a way to fail safe (an 'andon cord') if the model starts actively hurting the product

Similarities to General System Design

  • Still fundamentally a client-server architecture in most deployments — a client requests, a server (or serving endpoint) responds
  • The same scalability constraints apply: latency budgets and memory limits bound what a design can do, regardless of whether it's serving a model or a database query
  • The same requirements-first discipline applies — clarify before designing, quantify non-functional targets, confirm scope out loud
  • The same trade-off-driven reasoning applies — propose at least two options for a real fork and name why one was picked

Where It Actually Differs

  • The focus shifts toward the modeling task itself — metrics and evaluation criteria, and where the training data actually comes from
  • Data pipelines become a first-class design concern: batch vs. streaming ingestion, and transforming raw data into features the model can actually learn from
  • Training and serving infrastructure adds a new axis entirely — CPU vs. GPU, distributed training at scale (splitting the workload via data parallelism vs. model parallelism), and on-device vs. cloud inference latency
  • Not every ML system is client-server — on-device inference is a growing deployment mode with no server round-trip at all
  • Offline and online data can genuinely disagree — a model can look great against held-out data and still underperform once it's shaping what real users see
The GenAI-Specific Delta

The extra layer a generative model bolts on

Everything in the ML framework above assumes a model that classifies or predicts. A generative model adds one more layer on top of that: a model that also has to generate, not just predict — and a subsystem chained around it that a discriminative model never needed.

  1. 1

    Discriminative vs. Generative Models

    Before anything else: is this even a generative task? Discriminative models learn P(Y|X) to classify or predict — that's everything the ML framework above already covers. Generative models learn P(X) or P(X,Y) to sample new content instead.

    • Classical generative approaches (Naive Bayes, Hidden Markov Models) exist but struggle with unstructured data; modern GenAI is dominated by VAEs (encode → latent space → decode), GANs (generator vs. discriminator adversarial training), diffusion models (reverse denoising — dominant for image/video), and autoregressive models (predict the next token from what came before — dominant for text, and every production LLM)
    • This fork decides whether the rest of this section even applies: a fraud classifier or a ranking model stays a discriminative problem and belongs entirely in the framework above; a chatbot or image generator is generative, and everything below is the delta on top of that framework
  2. 2

    GenAI Risks — Raise Them Unprompted

    A strong candidate surfaces these before the interviewer asks — the same "raise it unprompted" instinct as security in the general framework, because a generative system has failure modes a discriminative one doesn't.

    • Hallucination: a confident, fluent, and wrong output with no ground-truth check at generation time — the single most-probed GenAI risk, and the reason grounding (see RAG below) is a mitigation pattern, not a nice-to-have
    • Deepfakes and adversarial misuse: synthetic media used for blackmail, political manipulation, or automated phishing — a risk that only exists once a model can generate convincing output, not just classify it
    • Environmental cost: training frontier models at scale is a real line item (GPT-4-class training runs are reported in the hundreds of millions of dollars), and a legitimate trade-off to name alongside retraining cadence
    • IP, misinformation, and training-data bias: outputs can reproduce copyrighted material, confidently state wrong facts, or reflect bias baked into the training corpus — distinct from the model-accuracy bias discussion in the ML framework's Evaluation step, because here the training data itself is the risk surface, not just the decision boundary
  3. 3

    RAG as a Subsystem, Not a Single Model Call

    Retrieval-Augmented Generation is the highest-leverage GenAI pattern to know cold: a retriever, a generator, and a grounding step composed together. This page's own AI chat already runs this shape — src/app/api/chat/route.ts and src/data/AiSystemInformation.ts.

    • Retriever: the chat route builds Gemini's systemInstruction by reading the résumé markdown straight off disk (fs.readFileSync in AiSystemInformation.ts) and injecting the whole document on every request — full-document injection instead of chunk-embed-search, because the corpus is one file. A larger corpus (a multi-tenant support bot, a codebase-wide assistant) needs the vector-store half of RAG instead of this.
    • Generator: a single Gemini call (model.startChat → sendMessageStream) streams the response back to the client — the language-model half of RAG, and the only half a bare "call an LLM" implementation would have
    • Grounding: the system instruction explicitly tells the model to answer from the injected résumé/portfolio context rather than free-associate — the reason RAG reduces hallucination isn't magic, it's that the model is answering an open-book question instead of an open-ended one
    • The retriever changes with scale (static context injection here vs. embed-once/nearest-neighbor search in a larger system) — the generator + grounding contract stays the same either way
  4. 4

    Overall GenAI System Design — The Missing Step

    The classical framework folds this into the ML framework's High-Level Architecture step above. GenAI earns it as a separate step because a generative core model is rarely deployed alone — content filtering, post-processing, and (for image/video) upscaling chain around it.

    • A generative core model chains together with content filtering (NSFW / harmful-content filters running pre- and post-generation), post-processing, and — for image or video output — upscaling / quality-enhancement models; the core model is one block in the pipeline, not the whole system
    • Scalability is the same lever as anywhere else on this page: load balancers and distributed inference in front of the model-serving tier, not a new concept — see the cross-reference block below
    • Security is the same lever too: privacy of personalized-generation inputs, adversarial-prompt attacks, model tampering, and data leakage through the generated output itself — see the cross-reference block below
    • User feedback loops and a retraining cadence close the pipeline — the same monitoring instinct as the ML framework's step above, with content-quality signals added to the drift signals already covered there
  5. 5

    Training & Sampling — Compact Reference

    Two more terms worth naming precisely if asked, kept intentionally brief — deep Transformer-internals and large-scale-training-efficiency questions are a different interview than "GenAI as part of a SWE system," and are only worth acknowledging here, not building out.

    • Multi-stage LLM training: pretraining (general language patterns from large public corpora, e.g. Common Crawl) → supervised fine-tuning (adapting to a specific task) → alignment (RLHF-style — aligning outputs to human preferences), each a distinct stage with its own data and objective
    • Sampling at inference time: greedy search (always the top token — fast, repetitive) vs. beam search (tracks multiple candidate sequences) vs. top-k / top-p (nucleus) sampling (samples from a truncated probability distribution) — the trade-off in every case is coherence vs. diversity
    • Explicitly out of scope here: Transformer self-attention math (Q/K/V, multi-head attention) and large-scale training efficiency (gradient checkpointing, mixed precision, ZeRO/FSDP) — real interview topics, but pure ML-theory/training-infra depth with the least connection to this page's SWE-system framing

How This Plugs Into the Rest of the Page

None of this replaces the SWE or ML content elsewhere on this page — it's the same components (load balancers, security, metrics) applied to a generative core instead of a discriminative one.

The Centerpiece

Ask the right question before picking an answer

Six categories, worked through in two passes. Design comes first and stays deliberately non-functional — it grounds every later decision in the core characteristics above before anything gets built. Data, Model, Backend, Frontend, and Ops then work through what each layer adds functionally on top of that baseline. Each category opens with a few guiding questions — a scannable primer, not the full picture — followed by the recurring decision cascades underneath: a question, the clarifying sub-questions that narrow it, the approach options on the table, implementation notes one layer deeper, and what else the decision ripples into.

Category

Design

  • Before any implementation detail — what shape is this system, and who owns each piece of it?
  • Which of the core characteristics above matters most for this system — and which is it explicitly willing to sacrifice?
  • What's the blast radius if one piece of this fails — does the design contain that, or let it cascade?

When the network partitions, what breaks — availability or consistency? And even when it doesn’t, what do you trade for lower latency?

  • If a partition happens, can this system reject writes/reads (favoring consistency) or must it keep serving (favoring availability)?
  • Absent a partition, is there still a latency-vs-consistency trade being made on every request (PACELC’s "else" clause)?
  • Is a partition actually likely for this deployment topology, or is this a single-region system where the P in CAP rarely fires in practice?

CP (consistency over availability)

When to use: Financial ledgers, inventory counts, anything where serving stale or conflicting data is worse than an error page.

Trade-offs: Nodes on the minority side of a partition refuse requests until they can confirm they have the latest state — real, user-visible downtime during a partition.

AP (availability over consistency)

When to use: Social feeds, shopping carts, presence indicators — anywhere a slightly stale read is acceptable and staying up matters more.

Trade-offs: Every node keeps serving during a partition, but different nodes can disagree until they reconcile — requires a conflict-resolution strategy (last-write-wins, CRDTs, application-level merge).

PACELC’s "else": latency vs consistency with no partition at all

When to use: Any system, at all times — this is the trade you’re making on the happy path, not just during failures.

Trade-offs: Synchronous replication for strong consistency adds round-trip latency to every write; async replication is fast but leaves a window where a reader can see stale data.

Go deeper

CAP is a partition-time statement, not a permanent system label

A system can be CP during a partition and still make a separate latency/consistency trade the rest of the time — PACELC exists precisely because CAP alone underspecifies steady-state behavior.

Should this ship as one deployable unit, or as independently-deployable services?

  • How many teams need to own and deploy different parts of this system independently?
  • Do different parts of the system have wildly different scaling profiles that a single deployable can’t serve efficiently?
  • Is the domain boundary between components actually well understood yet, or still in flux?

Monolith

When to use: Early-stage products, small teams, or domains still being discovered — one deployable, one datastore, simplest operational surface.

Trade-offs: Fast to build and reason about, but scaling and deploying one piece means scaling/deploying the whole thing.

Modular monolith

When to use: Team wants clear internal domain boundaries (for a future split) without paying distributed-systems tax today.

Trade-offs: Keeps deployment simple while enforcing module boundaries in code — the natural middle ground, and often underrated.

Microservices

When to use: Multiple teams need independent deploy cadences, or components have genuinely different scaling/reliability needs.

Trade-offs: Independent scaling and deployment, at the cost of network calls between services, distributed debugging, and real operational overhead.

Go deeper

Splitting too early is a common staff-level anti-pattern

Domain boundaries are usually clearer after a system has been in production for a while — premature microservices split along guessed boundaries that get expensive to redraw later.

Should this capability be built in-house, or adopted as an existing managed service, SaaS, or open-source library?

  • Is this capability actually the thing that differentiates the product, or is it commodity infrastructure every product needs?
  • Do mature, well-supported solutions already exist for this problem, or would adopting one mean bending the product around a poor fit?
  • What does the recurring cost (dollars, vendor risk, integration surface) of buying compare to the ongoing engineering cost of owning it forever?

Buy / adopt an existing solution

When to use: The capability is not a differentiator for this product and mature solutions already exist — auth, payments, rate limiting.

Trade-offs: Fast to ship and offloads ongoing maintenance to the vendor, but adds recurring cost and real vendor lock-in.

Build in-house

When to use: The capability IS the differentiator, or a hard constraint (e.g. a $0 budget) rules out every paid option.

Trade-offs: Full control and no recurring cost, but the team owns every bug, edge case, and security patch for as long as the system lives.

Hybrid — thin wrapper around an open-source or managed core

When to use: The common middle case: most of the problem is commodity, but a specific piece needs product-specific behavior.

Trade-offs: Balances speed and control, but needs an explicit customization boundary decided up front, or the "thin" wrapper quietly grows into a second implementation.

Go deeper

This mirrors a staff-level signal already tracked elsewhere on this page

The difference is deciding it here, for a specific system, not just recognizing it as a competency — see the "Shows build-vs-buy judgment" staff signal for the general pattern this question turns into a concrete decision cascade.

How replaceable is this component later, if the buy decision turns out wrong?

A buy decision behind a clean internal interface can be swapped for a build later at moderate cost; a buy decision woven directly through the codebase turns "wrong vendor" into a much larger rewrite than the original decision implied.

Before any code is written, does the underlying hardware and network reality change the shape of this design?

  • Is this workload dominated by CPU cycles, by moving data through memory, by network round trips, or by disk I/O — and does that change which component of the design needs to scale first?
  • Is that even knowable at design time, or does it only become clear once real traffic hits a running system and can be profiled?
  • If it’s not knowable yet, is the design at least built so a wrong initial guess (e.g. provisioning for compute when the real bottleneck is network) is cheap to correct later?

Compute-bound workloads

When to use: Heavy computation on data already in memory — encoding, dense ML inference, cryptographic hashing.

Trade-offs: More/faster cores and parallelization help directly, but Amdahl’s law caps the return on parallelizing a workload that has any sequential portion at all.

Memory/data-movement-bound workloads

When to use: The working set doesn’t fit in fast memory — large joins, wide scans, anything that spends more time shuffling data than computing on it.

Trade-offs: Better caching, columnar formats, and reducing data movement help; throwing more CPU cores at it does nothing if the cores are idle waiting on memory bandwidth.

Network-bound workloads

When to use: Latency dominated by round trips between services or regions, not by local computation — chatty microservices, cross-region calls.

Trade-offs: Batching requests and colocating services that talk to each other constantly cuts round trips; adding raw compute to either side does nothing about the wire time in between.

Disk/storage I/O-bound workloads

When to use: Random reads/writes against a dataset too large for memory — large transactional databases, log-heavy systems.

Trade-offs: Faster storage (SSD/NVMe) and higher IOPS provisioning help directly; more CPU or RAM elsewhere in the system won’t move data off disk any faster.

Go deeper

This is the same axis the Ops-stage "compute vs memory bound" question asks, applied earlier

That question diagnoses a bottleneck in a system that already exists and is running, using real profiling data. This one estimates the same thing before anything is built, when the only tools are back-of-envelope math and known-workload intuition — see the compute-vs-memory-bound question for the profiling-time version of the same question.

Back-of-envelope estimates need real reference numbers, not guesses

See the "Numbers to know" entry in the Design column of the Reference Grid for the concrete memory/disk/network latency figures this kind of estimate has to be grounded in.

Once identity and access boundaries are decided, how does that decision propagate through the system — because enforcement varies by layer, and retrofitting it later is expensive.

  • Does access control need to be decided once, at a single gateway-level check, or enforced independently at every layer it touches — DB row-level security, server-side session/JWT validation, API-gateway authorization?
  • When one internal service calls another on behalf of a user, does the downstream service re-validate that user’s access, or does it implicitly trust whatever the upstream service already checked?
  • If the answer to enforcement-per-layer changes later, how much of the system has to be touched to change it?

Enforce once, at the edge (API gateway / BFF)

When to use: Simple systems with a small number of trusted internal services and a single, well-defined entry point.

Trade-offs: Simplest to build and reason about, but every downstream service implicitly trusts the edge — a single compromised or misconfigured edge check is a total bypass for everything behind it.

Enforce at every layer independently (defense in depth)

When to use: Systems handling sensitive data, or with enough internal services that any one of them being wrong is a real risk.

Trade-offs: DB row-level security, service-level checks, and gateway-level checks all agreeing is much harder to bypass with a single mistake, but real duplication of authorization logic across layers, with a real risk the layers drift out of agreement over time.

Enforce at the service boundary that owns the data

When to use: Domain-driven systems where each service already owns a clear slice of data and wants to be the sole authority over who can touch it.

Trade-offs: Each service trusts only identity (who the caller is) from upstream, not authorization (what they’re allowed to do) — keeps authorization logic colocated with the data it protects, but means every service has to implement its own access checks rather than relying on a shared layer.

Go deeper

This is a design-phase decision, not a hardening pass done later

Retrofitting security boundaries into an architecture that was built without them is a much larger rewrite than deciding upfront which layer is the source of truth for authorization — the earlier this is decided, the cheaper every later layer is to build correctly against it.

Components of System Design

The concrete building blocks a request passes through and gets checked against before any application code runs — where traffic enters and gets routed, how it finds the system at all, and who's allowed to be making it — worked as full trade-off comparisons rather than named in passing.

Where does traffic get distributed, translated, or intercepted before it reaches a backend instance?

  • Is the goal purely spreading load across identical instances, or also routing/composing across different services?
  • Does this need to enforce cross-cutting concerns (auth, rate limiting, request transformation) in one place?
  • Is this internal, service-to-service traffic, or the public-facing edge of the system?
  • Does routing need application-layer awareness (HTTP headers/paths), or is transport-layer distribution enough?

Load balancer (L4/L7)

When to use: Distributing requests across many identical instances of the same service — the default first line of defense against any single instance being overwhelmed.

Trade-offs: Simple and fast, especially at L4, but has no concept of "service A vs service B" — it balances, it doesn't route by business logic.

API gateway

When to use: A single entry point in front of many downstream services — centralizing auth, rate limiting, request/response transformation, and path-based routing.

Trade-offs: Powerful and centralizes cross-cutting concerns, but adds a network hop and becomes a shared dependency that must itself be scaled and kept highly available.

Reverse proxy

When to use: A general-purpose intermediary in front of one or more servers — TLS termination, response caching, compression, and hiding backend topology from the outside.

Trade-offs: Flexible and often free (nginx, Envoy), but on its own doesn't understand business-level routing the way a purpose-built gateway does.

Go deeper

These three overlap heavily in practice

A single nginx or Envoy instance can act as reverse proxy, load balancer, and (with enough config) a crude API gateway — the distinction that matters in an interview is intent and where cross-cutting logic lives, not which specific product you'd buy. Managed API gateways (Kong, AWS API Gateway) exist because centralizing that logic by hand gets unwieldy at scale.

Before a request reaches any application code, how does it find the system and get served as cheaply as possible?

  • Is the content static/cacheable, or does it require a live backend response?
  • Does the system span multiple regions, and does traffic need to be routed to the nearest or healthiest one?
  • What's the acceptable failover time if a region goes down?

DNS resolution

When to use: Every request starts here — domain-to-IP resolution, and (with geo/latency-based routing or Anycast) the layer where traffic gets steered to the nearest or healthiest region before a single packet reaches your infrastructure.

Trade-offs: TTL caching makes DNS-level failover slow to propagate — minutes, not seconds — so it's a coarse, not real-time, lever.

CDN / edge caching

When to use: Static or cacheable content served from a location physically close to the user — the single biggest latency win for content that doesn't change per-request.

Trade-offs: Excellent for cache hit rates, but cache invalidation and the static/dynamic content split need a real strategy or stale content ships silently — see the Reference Grid's CDN / edge entry.

Go deeper

TCP vs UDP is a real fork, not just vocabulary to name-drop

TCP guarantees ordered, reliable delivery via handshake, acknowledgment, retransmission, and congestion control — but that reliability costs a connection setup and head-of-line blocking when a single packet is lost. UDP sends packets with no delivery or ordering guarantee at all, trading reliability for lower latency and zero handshake overhead. The fork that actually matters is whether the application can tolerate loss and reordering (DNS queries themselves, video/audio streaming, real-time gaming, where a late packet is worse than a dropped one) or genuinely can't (a file transfer, almost every typical API call). Worth naming if it comes up: HTTP/3 builds its own reliability semantics on top of UDP via QUIC, specifically to avoid paying for all of TCP's guarantees when only some are needed.

DNS resolution is a caching chain, not a single lookup — and it's why failover is minutes, not seconds

A lookup walks browser/OS cache → recursive resolver (the ISP's, or a public one like 8.8.8.8) → root server → TLD server → authoritative server for the domain, and each layer caches the result according to the record's TTL. A long TTL means fast repeat lookups and fewer round trips, but slow failover if the underlying IP needs to change; a short TTL trades the reverse. That's the direct mechanism behind this entry's own claim above that DNS-level failover is minutes, not seconds — it's a consequence of TTL caching at every layer of the chain, not an arbitrary limitation of DNS as a protocol.

Who is making this request, what are they allowed to do, and how is the whole surface actually protected?

  • Is identity established once (session) or on every request (stateless token)?
  • Do permissions depend on role, on specific attributes, or on a relationship to the resource?
  • Where does encryption need to apply — in transit only, at rest only, or both?

Authentication (AuthN)

When to use: Establishing identity — session cookies for traditional web apps, JWTs for stateless/distributed APIs, OAuth for delegated third-party access.

Trade-offs: JWTs scale statelessly across instances but can't be revoked before expiry without an extra deny-list; sessions are trivially revocable but need shared or sticky state.

Authorization (AuthZ)

When to use: Once identity is established, deciding what that identity is actually allowed to do.

Trade-offs: RBAC/ABAC/ReBAC trade simplicity for flexibility in different ways — see the Reference Grid's AuthN vs AuthZ entry for the full comparison.

Security as the umbrella

When to use: The practices that protect the whole surface regardless of who's asking — TLS in transit, encryption at rest, input validation against the OWASP Top 10.

Trade-offs: None of these are optional trade-offs so much as a floor — see the Reference Grid's Transport encryption, Network security, and Input validation entries.

Go deeper

AuthN and AuthZ are sequential, not interchangeable

Authorization always assumes authentication already happened — a system can't decide what you're allowed to do until it knows who you are. Conflating the two in an interview answer is a common signal of not having the distinction crisp.

Category

Data

  • What guarantees does this data actually need — and what do you trade away to get them?
  • Does every reader need to agree on the same value at the same moment, or can different parts of the system tolerate seeing a slightly different, slightly older answer?
  • Is the shape of this data fixed and known ahead of time, or does it vary enough that forcing one rigid schema on it would fight the data itself?
  • Once a single database can't hold or serve all of it, does it get reorganized for a different purpose — reporting, search, historical analysis — or spread wider: sharded, cached, queued, or streamed?

How stale can a read be, and does it matter whose read it is?

  • Does the reader need to see their own writes immediately, even if other users’ writes lag (read-your-writes)?
  • Does the ordering of causally related events need to be preserved for correctness (causal consistency), or is any eventual order fine?
  • Is this a single-writer/single-reader path, or many concurrent writers where conflicts are actually possible?

Eventual consistency

When to use: Analytics counters, follower counts, cache-backed reads — anywhere a few seconds of staleness is invisible to the user.

Trade-offs: Cheapest and most available option, but offers no ordering or freshness guarantee at all.

Monotonic reads

When to use: A user paging through results — guarantees they never see an older value after a newer one.

Trade-offs: Needs session/sticky routing to the same replica or a version token passed between requests.

Read-your-writes

When to use: A user edits their profile and immediately reloads the page — they must see their own edit.

Trade-offs: Typically requires routing that user’s reads to the primary (or a replica confirmed caught-up) right after a write.

Causal consistency

When to use: A comment reply must never render before the comment it replies to.

Trade-offs: Requires tracking causal metadata (vector clocks or similar) — more bookkeeping than read-your-writes.

Strong / linearizable consistency

When to use: Bank balance checks, distributed locks, leader election — anywhere every reader must agree on the single latest value.

Trade-offs: Highest coordination cost and latency of the spectrum; usually means routing everything through a single source of truth.

Go deeper

Picking a point on the spectrum, not just the two extremes

Most production systems mix levels per data type — strong consistency for the payment record, eventual for the "likes" counter next to it. Naming which parts of the system sit where is more convincing than picking one setting for everything.

Is this structured, relational data that needs transactional guarantees, or flexible-schema data that prioritizes availability and scale?

  • Does the data have well-defined relationships (foreign keys, joins) that the application relies on?
  • Do multi-row/multi-table operations need to succeed or fail atomically (ACID transactions)?
  • Is the schema expected to change frequently, or per-record, in ways a rigid relational schema would fight against?

SQL / relational (ACID)

When to use: Orders, payments, anything with real relationships and a need for multi-table transactions to stay atomic.

Trade-offs: Strong guarantees and mature tooling, but schema changes are heavier and horizontal write-scaling is harder.

NoSQL / BASE (Basically Available, Soft state, Eventually consistent)

When to use: High-volume, loosely-structured, or rapidly-evolving data — session state, event logs, product catalogs with variable attributes.

Trade-offs: Scales out easily and schema is flexible, but you give up cross-record transactional guarantees by default.

Go deeper

This is not strictly binary in practice

Modern relational databases (Postgres with JSONB, for example) borrow NoSQL-style flexibility, and many NoSQL stores now offer limited transactions — the underlying question (do I need cross-record atomicity and joins?) matters more than the SQL/NoSQL label itself.

Walking the decision tree: what’s the access pattern, the write:read ratio, the consistency requirement, and the scale target?

  • Access pattern first — is this key-value lookups, full-text search, graph traversal, or time-ordered writes?
  • What’s the write:read ratio — write-heavy (event ingestion) or read-heavy (product pages)?
  • What consistency level does this data actually need (see the consistency-spectrum question above)?
  • Are there data-residency or compliance constraints (GDPR, HIPAA, SOC2) that limit which regions/vendors are even eligible?

Relational (Postgres/MySQL)

When to use: Structured data with relationships and transactional needs — the default unless something rules it out.

Trade-offs: Vertical scaling is the easy path; horizontal write-scaling needs sharding, which adds real complexity.

Key-value (Redis, DynamoDB)

When to use: Simple lookups by a known key at very high throughput — sessions, caches, feature flags.

Trade-offs: Extremely fast and simple, but no relational queries or joins.

Document (MongoDB)

When to use: Semi-structured, nested data that maps naturally to JSON and doesn’t need complex joins.

Trade-offs: Flexible schema speeds up iteration, but data duplication across documents can make updates harder to keep consistent.

Column-family (Cassandra, Bigtable)

When to use: Massive write throughput with known query patterns, e.g. time-series ingestion at scale.

Trade-offs: Excellent write scalability, but query flexibility is limited to patterns designed for upfront.

Search index (Elasticsearch)

When to use: Full-text search, fuzzy matching, faceted filtering.

Trade-offs: Best-in-class search UX, but it’s an eventually-consistent secondary index — not a system of record.

Graph (Neo4j)

When to use: Deeply relational, traversal-heavy queries — social graphs, recommendation paths.

Trade-offs: Graph traversals that would be expensive multi-join SQL queries become cheap, but it’s a less common tool with a smaller hiring pool.

Go deeper

Compliance/data-residency as an early filter, not an afterthought

If GDPR/HIPAA/SOC2 apply, they can rule out entire hosting regions or vendors before performance characteristics even enter the conversation — worth asking about explicitly in the requirements step, not discovering during the deep dive.

The tree is sequential, not a single lookup table

Access pattern narrows the field first; write:read ratio and consistency needs then pick among the remaining candidates — jumping straight to "which database" without walking the earlier steps is the most common shortcut that gets called out in interviews.

Does this data have a fixed, known schema, or is it fundamentally unstructured?

  • Can every field be named and typed ahead of time, or does the shape vary record-to-record?
  • Is this text/binary content meant to be indexed and searched, or stored and retrieved as a blob?
  • Will downstream consumers need to query on internal fields, or only fetch the whole object?

Structured — relational or strongly-typed document store

When to use: Rows/columns or JSON with a stable, known shape — user records, orders, config.

Trade-offs: Enables indexing, joins, and validation at write time, at the cost of schema rigidity.

Unstructured — object storage / data lake

When to use: Logs, images, video, free-text documents — content without a fixed schema.

Trade-offs: Cheap, infinitely flexible storage, but querying internal structure requires a separate indexing/processing layer on top.

Semi-structured — search index or wide-column store

When to use: Text or logs that still need to be queried/filtered, not just archived.

Trade-offs: Bridges the gap, but is a secondary/derived store, not typically the system of record.

Go deeper

This is a genuinely separate axis from SQL vs. NoSQL

A NoSQL document store can still hold fully structured data (a known JSON shape); the real question here is whether the schema is knowable at all, not which query language sits on top of it.

Is this system serving live transactional reads/writes for a product, or aggregating historical data for reporting and analytics?

  • Are queries mostly single-row lookups/updates (transactional), or large scans/aggregations across many rows (analytical)?
  • Does this need to be up-to-the-second, or is nightly/hourly freshness acceptable?
  • Are the read and write workloads from the same system contending for the same resources today, and is that contention actually a problem?

OLTP — row-oriented transactional database

When to use: The live product path: checkout, profile updates, inventory decrements.

Trade-offs: Optimized for fast single-row reads/writes, but large aggregate queries across millions of rows are slow and can degrade the transactional workload.

OLAP — column-oriented warehouse

When to use: Dashboards, BI reporting, large historical aggregations.

Trade-offs: Excellent at scanning/aggregating huge datasets, but not designed for high-frequency single-row transactional writes.

Go deeper

The usual fix is separation, not a single database doing both

The standard pattern is OLTP as the system of record with a periodic ETL/CDC pipeline feeding an OLAP warehouse — this keeps analytical query load from ever touching the live transactional path.

What's the next cheapest lever before reaching for the expensive one?

  • Have the cheap, low-risk levers (indexing, query optimization) actually been exhausted, or is this jumping straight to sharding?
  • Is the bottleneck reads, writes, or both — different levers help different sides.
  • What would it cost (in engineering time and operational complexity) to reach for the next lever, versus the traffic growth it buys?

Index optimization

When to use: First lever, almost always — a missing or wrong index is the most common source of slow queries.

Trade-offs: Cheap and reversible, but only fixes query-shape problems, not raw volume.

Query optimization

When to use: After indexing — rewriting N+1 queries, reducing over-fetching.

Trade-offs: Still cheap, still reversible, still limited to inefficiency rather than true capacity limits.

Connection pooling

When to use: When connection overhead itself is the bottleneck under concurrent load.

Trade-offs: Low effort, meaningful win under high concurrency, doesn’t help raw data volume.

Caching

When to use: Read-heavy workloads with repeated queries for the same data.

Trade-offs: Big win for read latency and DB load, but introduces cache-invalidation complexity.

Read replicas

When to use: Read traffic outgrowing a single primary’s capacity.

Trade-offs: Scales reads horizontally, but replicas lag behind the primary — reintroduces the consistency-spectrum question.

Vertical scaling

When to use: A quick, temporary lever to buy time before a structural fix.

Trade-offs: Fast and simple, but has a hard ceiling and doesn’t address the underlying access pattern.

Sharding

When to use: Write volume has outgrown what a single primary can handle, after the cheaper levers are exhausted.

Trade-offs: Real horizontal write scaling, at the cost of cross-shard queries and transactions becoming much harder.

DB type change

When to use: The access pattern has fundamentally outgrown the current database category, not just its current scale.

Trade-offs: The most expensive lever — a migration — but sometimes the honest answer once every cheaper lever is spent.

Go deeper

Naming this ladder explicitly is itself a signal

Jumping straight to "we’d shard the database" without naming the cheaper steps first reads as reaching for a big-sounding answer rather than reasoning from first principles — walking the ladder out loud is the stronger move.

When the cache is full and a new entry needs to be inserted, which existing entry gets evicted?

  • Is access to cached entries roughly uniform, or is it skewed/long-tail with a small hot set?
  • Does recency (accessed most recently) or frequency (accessed most often) better predict what gets accessed next?
  • Does eviction need to be cheap — O(1) — to keep up at high throughput, or is there headroom for a heavier bookkeeping structure?

FIFO

When to use: The simplest possible policy — a plain queue, evicting the oldest entry regardless of how hot it is.

Trade-offs: Trivial to implement, but ignores access pattern entirely — a frequently-hit entry gets evicted just as readily as a cold one.

LRU (Least Recently Used)

When to use: The default in practice — O(1) eviction via a hash map plus a doubly-linked list.

Trade-offs: Cheap and usually a good approximation of "hot," but a burst of one-off scans can evict an otherwise-hot working set — cache pollution.

LFU (Least Frequently Used)

When to use: Skewed/long-tail access patterns where LRU's cache pollution problem is a real, recurring issue.

Trade-offs: Resists that pollution problem by tracking access frequency, but needs a decay strategy — otherwise a once-popular-now-cold key pins itself in the cache forever.

TTL (Time To Live)

When to use: Data has a natural expiry — session tokens, rate-limit counters, anything that's wrong (not just stale) past a certain age.

Trade-offs: Correctness-driven rather than access-pattern-driven — expires an entry on a schedule regardless of how hot it still is; usually layered on top of LRU/LFU rather than used as the sole policy.

Random

When to use: Extremely high-throughput caches where even LRU/LFU's O(1) bookkeeping overhead is worth avoiding.

Trade-offs: Cheapest possible eviction — no access metadata to maintain at all — but with zero access-pattern awareness, hit rate is whatever the workload happens to give you.

Go deeper

Eviction policy and cache write pattern are orthogonal choices

Which entry gets evicted (FIFO/LRU/LFU) is independent from how writes propagate to the cache (cache-aside/write-through/write-behind/write-around — see the Reference Grid's Cache write patterns entry). Most real caches, Redis or a CDN edge node, let you pick one from each axis independently.

When traffic or data needs to be assigned to one of many nodes — and nodes are added or removed over time — how do you avoid remapping almost everything on every change?

  • Is the node set stable, or elastic — nodes routinely added/removed for scaling or failure recovery?
  • Does a rebalance need to move the minimum possible amount of data/traffic, or is a full remap acceptable?
  • Is node capacity uniform across the fleet, or are some nodes meaningfully bigger/smaller than others?

Naive modulo hashing

When to use: A fixed, never-changing node count — the trivial case.

Trade-offs: `hash(key) % N` is trivial to implement, but changing N — adding or removing even one node — remaps nearly every key.

Consistent hashing (hash ring)

When to use: Node counts that change over time and where minimizing remap churn on each change actually matters.

Trade-offs: Nodes and keys both map onto a ring; adding or removing one node only remaps the keys between it and its ring neighbor — roughly 1/N of all keys, not nearly all of them.

Consistent hashing + virtual nodes

When to use: The same elastic-node-set problem, but with uneven physical node capacity.

Trade-offs: Each physical node gets many points on the ring, smoothing load distribution when capacity is uneven and reducing the odds of one node inheriting a disproportionate share on rebalance.

Go deeper

The same mechanism solves two different-looking problems

CDN edge-node request routing (maximizing cache-hit locality as edge nodes are added/removed) and cache/database shard ownership (minimizing data movement on scale-in/out) are the same "assign X to one of many nodes cheaply, with minimal churn on change" problem, solved twice.

This composes with — doesn't replace — the CAP/PACELC trade-off

Consistent hashing decides where a key lives (see the CAP vs PACELC question for what consistency guarantee reads/writes to it get) — it answers routing, not consistency.

Once data lands somewhere durable, what architecture organizes it for consumption — and is the constraint technical or organizational?

  • Is the data's schema known upfront, or does it need to be discovered/imposed later, at read time?
  • Is the actual bottleneck query performance and governance (technical), or which team owns and can change the data (organizational)?
  • Is the source data static/reference — a one-time load — or an unbounded, continuously-arriving stream?

Data lake

When to use: Raw or unstructured data landing cheaply via object storage, with schema imposed later at read time.

Trade-offs: Cheap and flexible, schema-on-read — but ungoverned, it degrades into a "data swamp" nobody trusts.

Data warehouse

When to use: Fast, structured BI/analytics queries against a fixed, enforced schema — schema-on-write.

Trade-offs: Fast, well-governed structured queries, but every new source needs upfront modeling before it's usable.

Data lakehouse / Delta Lake

When to use: Wanting warehouse-style guarantees without the separate lake-to-warehouse ETL hop.

Trade-offs: Adds warehouse-style ACID transactions and schema enforcement directly on top of lake object storage, collapsing that ETL hop — but the tooling is younger and still maturing.

Data mesh

When to use: The bottleneck is organizational, not technical — a single central team owning one warehouse is the actual constraint.

Trade-offs: Domain teams own and publish their own data as products instead of a central team owning one warehouse — but this only works with real platform/tooling investment behind it, or it just relabels the same bottleneck under a new name.

Go deeper

This layers on top of, not replaces, the row/column and schema-timing questions

This question sits above `oltp-vs-olap` (row vs. column storage engine choice) and `structured-vs-unstructured` (whether the schema is knowable at write time) — it's about the organizing architecture around wherever that data already lives. See the Glossary's Data Warehouse, Data Lake, Data Lakehouse, and Data Mesh definitions (`src/constants/glossary/dataEngineering.ts`) for the first-principles terminology this question builds on.

Static/reference data vs. dynamic/streaming data changes the scope of the decision

A one-time load is a one-time architectural decision; continuously-arriving data needs a real ongoing ingestion pipeline with schema-evolution and backfill handling — see the `mapreduce-vs-stream-processing` question for that fork.

Should this table be decomposed further to eliminate redundancy, or is some redundancy an acceptable trade for read performance?

  • Does every non-key column depend on the whole primary key, or only part of a composite key (partial dependency)?
  • Do any non-key columns depend on each other rather than directly on the key (transitive dependency)?
  • Is this schema serving a write-heavy transactional path, or a read-heavy path where some redundancy might be a deliberate, faster trade?

First Normal Form (1NF)

When to use: The baseline for any relational table — every cell holds a single atomic value, no repeating groups.

Trade-offs: Necessary starting point, but doesn't remove redundancy on its own — that's what the next forms do.

Second Normal Form (2NF)

When to use: Tables with a composite primary key, where a non-key column only actually depends on part of it.

Trade-offs: Removes partial-dependency redundancy by splitting the table, but doesn't yet catch redundancy between non-key columns.

Third Normal Form (3NF)

When to use: The practical default target for most OLTP schemas — no non-key column depends on another non-key column.

Trade-offs: Removes transitive-dependency redundancy; most production schemas stop here since it's the best trade of integrity vs. query complexity.

Boyce-Codd Normal Form (BCNF)

When to use: Tables with multiple overlapping candidate keys, where 3NF alone still allows an anomaly.

Trade-offs: The strictest common form — every determinant is a candidate key — but stricter normalization means more joins to reassemble a full record.

Deliberate denormalization

When to use: Read-heavy paths where the extra joins a normalized schema requires are the actual bottleneck (see the Scaling progression question).

Trade-offs: Trades write-time integrity risk and storage duplication for fewer joins and faster reads — a conscious reversal of everything above, not a failure to normalize.

Go deeper

Higher isn't automatically better

Each step up trades write-side integrity and storage efficiency for more joins on read — 3NF is the common stopping point for OLTP schemas precisely because BCNF's marginal integrity gain rarely justifies its added query complexity.

Components of System Design

The concrete building blocks underneath any database choice — the physical medium data actually sits on, and how it or the load against it gets split across a boundary — worked as full trade-off comparisons rather than named in passing.

What's the underlying storage medium for this data, independent of what database sits on top of it?

  • Is this large, mostly-immutable content (media, backups, logs), or small records needing low-latency random access?
  • Does more than one instance need concurrent access to the same files, or is it exclusive to one attached instance?
  • How often does the content change after it's first written?

Object storage (S3-style)

When to use: Large, infrequently-modified blobs — images, video, backups, data-lake files — accessed by key, not by path.

Trade-offs: Durable (S3-class services advertise ~99.999999999% — "11 nines" — object durability via automatic cross-AZ replication) and effectively infinitely scalable at low cost, but no partial-file edits and higher per-request latency than block storage.

Block storage (EBS-style)

When to use: Low-latency, random-access disk for a database or any workload needing filesystem semantics on one instance.

Trade-offs: Fast, but tied to a single attached instance at a time — not natively shareable across a fleet.

File storage (EFS/NFS-style)

When to use: A shared, POSIX-compliant filesystem multiple instances need to read/write concurrently.

Trade-offs: Simplifies sharing, but adds network-filesystem latency and its own scaling ceiling compared to block storage.

Go deeper

This is a different axis than the Reference Grid's "Database taxonomy"

Database taxonomy is about data model — relational vs NoSQL, and so on. Storage type is about the medium underneath any of those — a relational database still needs to sit on block storage, and its backups still need to land somewhere object storage makes sense.

Is this a general split of data or load across a boundary, or specifically a distributed-database sharding strategy?

  • Is the split by responsibility/domain (functional), by column (vertical), or by row across identical nodes (horizontal)?
  • Has this outgrown a single database instance, or is it still one database being reorganized internally?
  • What's the shard/partition key, and does it risk creating a hot spot?

Functional / vertical partitioning

When to use: Splitting by responsibility — a users table on one database, orders on another — usually the first, cheapest lever before any horizontal split.

Trade-offs: Simple and requires no new distributed-systems infrastructure, but has a hard ceiling: it doesn't help once one functional area itself outgrows a single node.

Horizontal sharding (range/hash/directory)

When to use: One functional area's write volume has outgrown a single primary — the next lever once vertical partitioning is exhausted.

Trade-offs: Real horizontal write scaling, at the cost of cross-shard queries and transactions becoming much harder — see the Reference Grid's Sharding strategies entry for the range vs hash vs directory trade-off.

Directory-based partitioning

When to use: Shard assignment needs to change without re-hashing everything — a lookup service maps keys to shards instead of a fixed formula.

Trade-offs: Most flexible for rebalancing, but the lookup service itself becomes a new dependency and potential bottleneck.

Go deeper

Partitioning is the general concept; sharding is one specific case of it

Every sharding strategy is a horizontal partitioning strategy, but not every partitioning strategy is sharding — vertical/functional partitioning splits a schema or a system, not necessarily across a fleet of identical database nodes. Naming which one you mean avoids a common interview ambiguity.

Big Data

Data too large in volume, or arriving too fast, for one machine to process synchronously — message queues buffer the surge, MapReduce distributes the computation across a cluster.

When a surge of requests arrives faster than your servers can process them synchronously, how does a message queue actually get each one to a consumer?

  • Does the work need to happen immediately, or can it be queued and processed slightly later without the user noticing?
  • Is scaling the consumer horizontally or vertically actually the cheaper fix here, or is queuing genuinely more cost-effective for this traffic pattern?
  • If a consumer crashes mid-processing, does the message need to be redelivered, or is losing it acceptable?

Pull-based consumption

When to use: The consumer monitors the queue itself and pulls a new message only when it has spare capacity — Kafka-style consumers are the classic example.

Trade-offs: Keeps the consumer in control of its own load, so it's never overwhelmed, but an empty queue with a slow polling interval adds latency before a waiting message gets picked up.

Push-based delivery

When to use: The queue delivers a message to the consumer as soon as one is available — SQS long-polling and RabbitMQ delivery both work this way.

Trade-offs: Lower latency between message arrival and processing, but a burst of incoming messages can push faster than a fixed consumer fleet can keep up, risking overload.

Go deeper

Delivery isn't confirmed until the consumer says so

Whichever model delivers the message, the consumer sends an acknowledgement back once it's finished processing — the same ack mechanism as reliable network delivery. If the queue doesn't see an ack within a timeout window, it assumes the consumer failed and redelivers the message, which is why consumers of at-least-once queues need to be idempotent — see Idempotency in the Backend column.

Does exactly one consumer need to process each message, or should many independent services all react to the same event?

  • When this event fires, is there one clear owner of the resulting work, or do multiple unrelated services each need to know about it?
  • Should a new consumer be addable later without changing anything the publisher does?
  • Do consumers need to process at their own pace, independently of each other?

Point-to-point queue

When to use: A single logical operation with one owner — e.g. exactly one worker fleet processing payment jobs from a queue.

Trade-offs: Simple and load-balances naturally across a consumer fleet, but doesn't fan an event out to multiple independent interested parties.

Publish/subscribe (topic-based)

When to use: One event needs to reach several independent subscribers who don't need to know about each other — placing an order might need to update inventory, trigger billing, and notify shipping, all from one published event.

Trade-offs: New subscribers can be added to a topic without changing the publisher at all, which is the model's biggest strength — but message ordering across independent subscribers isn't guaranteed unless the broker specifically supports it.

Go deeper

The publisher never needs to know who is listening

A publisher only knows it's sending to a topic — it has no awareness of which, or how many, subscribers exist. That decoupling is what lets a system add a completely new consumer for an existing event without touching the code that publishes it, at the cost of the publisher having no guarantee its message was actually acted on by anyone in particular.

Is this a general-purpose big-data processing pattern — batch jobs across a cluster, or a continuous stream pipeline — independent of whether the output feeds a dashboard, a warehouse, or an ML feature store?

  • How fresh does the output need to be — minutes/hours-old is fine, or does it need to be near-real-time?
  • Does the computation need the full dataset at once, or can it process one event at a time as it arrives?
  • Can the team absorb the operational complexity of a stream processor — state management, exactly-once semantics, backpressure?

MapReduce-style batch (Hadoop/Spark)

When to use: Mature, well-understood tooling and a simple map -> shuffle -> reduce mental model.

Trade-offs: Output freshness is bounded by the run schedule, and a full-dataset job pays a roughly fixed cost every run — even for a small incremental delta.

Stream processing (Kafka Streams, Flink)

When to use: Near-real-time output is a real requirement, not a nice-to-have.

Trade-offs: Real operational complexity: state management, exactly-once semantics, backpressure, handling out-of-order events.

Micro-batch (Spark Structured Streaming)

When to use: Wanting most of streaming's simplicity of reasoning without committing to a full stream processor.

Trade-offs: Splits the difference, but is still fundamentally bounded by its micro-batch interval.

Go deeper

This is the general pattern; ML feature computation is one specific instance of it

MapReduce popularized the "split a big job across a cluster of commodity machines" pattern that both traditional OLAP-warehouse ETL jobs and ML feature-computation pipelines are built on — the existing `batch-vs-streaming` question (ML-feature-computation-scoped) is the ML-specific instance of this same general fork, not a separate concept.

What actually happens inside a MapReduce job

A master node splits the input and assigns chunks to worker nodes, then coordinates the job and reassigns work if a worker fails. Each worker's Map phase transforms its chunk into key-value pairs (e.g. mapping a word to a count of 1 per occurrence). A Shuffle-and-Sort phase then regroups those pairs across the cluster so every value for the same key ends up together, regardless of which worker originally produced it. Finally, each worker's Reduce phase aggregates the grouped values for its keys into a final result (e.g. summing all the 1s for a given word into its total count) — the same shape underlies redacting a column across billions of rows just as easily as counting words across a library.

Category

Model

  • What does putting a model into production add on top of a standard system design — and what stays exactly the same?
  • How do you know this model is good enough to ship — and how do you know it's still good after the world it was trained on has moved on?
  • Does a prediction need to be computed the instant it's asked for, or can it be precomputed ahead of time and just looked up?

Before this model ships, how do you know it's actually good — and how do you know it's still good once it's live?

  • What held-out data and metric (precision/recall/AUC, RMSE, etc.) will gate the ship decision, decided before training even starts?
  • What live, business-facing metric (CTR, conversion, revenue) will actually prove the model helped, once it's serving real traffic?
  • Is the training/eval data class-imbalanced or otherwise unrepresentative of live traffic in a way that could make offline metrics misleading?

Offline metrics

When to use: Gating the ship/no-ship decision — the last checkpoint before a model reaches real users, measured against a held-out test set.

Trade-offs: Cheap, fast, and reproducible, but measures fit to historical data — not what happens once the model's own predictions start changing user behavior.

Online metrics

When to use: The real, business-facing measure of whether the model helped — what the ML deployment risk ladder above is actually gating for.

Trade-offs: Measures true impact, including feedback loops offline metrics can't see, but takes real traffic, real time, and a live experiment to get a read.

Both, in sequence

When to use: The standard approach for any production model — offline gates deployment, online then validates it actually worked.

Trade-offs: Offline metrics can look great while online metrics disagree (the "offline/online gap") — a model that improved held-out accuracy can still lose on the metric that actually matters live.

Go deeper

Offline and online metrics can diverge — and often do

Class imbalance, label noise, and a training distribution that no longer matches live traffic are the usual causes of the offline/online gap — a model can top the held-out benchmark and still underperform once it's actually shaping what users see.

The right offline metric depends on the task type

Classification leans on Precision/Recall/F1/Accuracy plus ROC-AUC (or PR-AUC when classes are imbalanced) and a confusion matrix; regression uses MSE/MAE/RMSE; ranking uses Precision@k/Recall@k/MRR/mAP/nDCG — naming the wrong family for the task type is a quick tell that the metric was picked by habit, not by fit.

Should this data be processed in scheduled batches, or continuously as it arrives?

  • How fresh does the processed output need to be for downstream consumers?
  • Does the processing logic need to see the full dataset at once (batch), or can it operate on one event at a time (streaming)?
  • What’s the volume and arrival pattern of the data — steady trickle, or large periodic dumps?

Batch processing

When to use: Nightly feature computation, model retraining, large historical aggregations.

Trade-offs: Simpler to reason about and debug, but output is only as fresh as the last batch run.

Streaming processing

When to use: Real-time fraud scoring, live recommendation updates, anomaly detection.

Trade-offs: Low-latency output, but adds real operational complexity (stream processors, exactly-once semantics, backpressure handling).

Go deeper

Batch and streaming often coexist for the same pipeline

A common pattern is a streaming path for immediate approximate results plus a nightly batch job that recomputes exact features — the "lambda architecture" trade-off between freshness and correctness.

Does inference happen per-request in real time, or precomputed on a schedule?

  • Does the prediction depend on features only known at request time (current session context), or can it be precomputed ahead of need?
  • What latency budget does the serving path have — milliseconds, or is a lookup against precomputed results fine?
  • What’s the request volume — precomputing for every possible input may be cheaper than serving each on demand, or vastly more wasteful, depending on the input space size.

Online (real-time) prediction

When to use: Fraud detection at checkout, live ranking, anything needing request-time context the model hasn’t seen before.

Trade-offs: Always fresh and context-aware, but must meet a tight latency SLA on every request and needs a always-on serving infrastructure.

Batch (offline) prediction

When to use: Daily churn scores, weekly recommendation refreshes — anywhere precomputing for all known entities is feasible.

Trade-offs: Cheap and simple to serve (just a lookup), but predictions are stale between batch runs and can’t react to request-time context.

Go deeper

The input space size drives which is actually cheaper

Precomputing predictions for every user nightly only makes sense if the user base is bounded and mostly stable — an unbounded or highly dynamic input space (arbitrary search queries) usually forces online prediction regardless of latency preferences.

Is the input distribution shifting, the input-to-label relationship shifting, or is the model simply out of date?

  • Have the statistical properties of incoming inputs changed since training (data drift), even if the underlying relationship to the label hasn’t?
  • Has the actual relationship between inputs and the correct label changed (concept drift) — e.g. user preferences genuinely shifted?
  • Is performance degrading simply because the model hasn’t been retrained on recent data at all (staleness), independent of any distribution shift?

Monitor for data drift

When to use: Detect when incoming feature distributions diverge from training-time distributions.

Trade-offs: Catches upstream data pipeline changes and population shifts early, but a drift alert alone doesn’t tell you if it’s actually hurting predictions.

Monitor for concept drift

When to use: Detect when the model’s predictions are becoming less accurate against ground truth, even if input distributions look stable.

Trade-offs: Directly measures what matters (accuracy), but requires timely ground-truth labels, which aren’t always available quickly.

Scheduled retraining against staleness

When to use: A baseline safeguard regardless of whether drift is detected — the world moves even without a dramatic shift.

Trade-offs: Simple and predictable, but retraining on a fixed schedule can be wasteful if nothing has actually changed, or too infrequent if it has.

Go deeper

These three failure modes need different monitoring signals, not one dashboard

Data drift is measured by comparing feature distributions; concept drift needs ground-truth labels to compare against predictions; staleness is just a clock — conflating them leads to alerting on the wrong thing.

How much production traffic should see this new model before it fully replaces the old one?

  • Can the new model’s predictions be evaluated against real traffic without actually affecting what users see (shadow mode)?
  • What’s an acceptable blast radius if the new model performs worse than expected — a small percentage of users, or none until fully validated?
  • What’s the rollback plan if a regression is caught after the new model is already serving live traffic?

Shadow deployment

When to use: First step for any new model — run it alongside the current model on real traffic, log its predictions, but never serve them.

Trade-offs: Zero user-facing risk, but doesn’t validate real-world behavioral impact (e.g. how users react to different recommendations).

Canary rollout

When to use: After shadow validation looks good — serve the new model to a small percentage of real traffic.

Trade-offs: Bounds the blast radius of a bad model, but needs strong monitoring and a fast rollback path to be safe.

A/B test

When to use: Once safety is established and the question shifts to "is this actually better," measured against a real business metric.

Trade-offs: Gives a statistically grounded answer on impact, but takes longer to reach significance and needs careful experiment design to avoid contamination.

Go deeper

This mirrors general software deployment risk ladders

Shadow → canary → A/B is the ML-specific analog of blue-green → canary → rolling → feature-flags in general software deployment — worth naming that parallel explicitly, it signals the same risk-management instinct applies across both domains.

Deployment risk isn't just traffic ramp — inference cost is part of the same decision

Model compression trades a small accuracy hit for materially lower serving latency and cost: knowledge distillation (a small student model mimics the trained teacher), pruning (zeroing out least-useful parameters), or quantization (fewer bits per parameter, at training time or post-training) — worth proposing before assuming the trained model ships as-is.

Category

Backend

  • How do components talk to each other, and to the client, on what timing?
  • Once something changes on the backend, does everyone who cares find out immediately, or only when they next ask?
  • When one component fails mid-call, does the caller wait and retry, fail loudly right away, or never find out at all?

Does the consumer need this the instant it changes, or is periodic/on-demand delivery acceptable?

  • What's the acceptable staleness window — seconds, minutes, hours?
  • Is the consumer push-capable (can hold a connection open) or pull-only?
  • Is this client-facing (browser/mobile) or internal service-to-service?
  • What's the fan-out — one consumer, thousands, millions?

WebSockets

When to use: Client-facing, real-time, bidirectional — chat, collaborative editing, anything needing client→server AND server→client.

Trade-offs: Stateful connection; needs a pub/sub backplane (Redis, NATS) to broadcast across horizontally-scaled server nodes.

Server-Sent Events (SSE)

When to use: Client-facing, real-time, server→client only — live feeds, notifications, progress updates.

Trade-offs: Simpler than WebSockets, HTTP-native, auto-reconnect built in — but one-directional only.

Long polling

When to use: Real-time delivery under compatibility constraints — legacy clients or restrictive networks/firewalls that block persistent connections.

Trade-offs: Highest latency and server overhead of the three, but the most broadly compatible.

REST or GraphQL

When to use: Batch-acceptable, client-facing — standard CRUD/resource access where staleness of seconds-to-minutes is fine.

Trade-offs: REST is simple and cacheable; GraphQL avoids over/under-fetching at the cost of query complexity.

gRPC over REST

When to use: Internal, performance-sensitive service-to-service calls where latency and strongly-typed contracts matter.

Trade-offs: HTTP/2 multiplexing and binary protobuf payloads beat REST/JSON on latency and payload size, but it's not browser-native and needs a gateway/BFF translation layer for client-facing use.

Go deeper

Language/runtime choice for gRPC services

gRPC's codegen and tooling story favors statically-typed languages — Go and Java are common defaults in the ecosystem. Python and TypeScript work fine too but often carry more runtime/tooling overhead for high-throughput internal services. This is highly context-dependent: a team's existing language stack, hiring pool, and existing service-mesh tooling usually outweigh the theoretical 'best' choice — reasonable engineers differ here.

Scaling WebSockets horizontally

A single app server can hold a bounded number of open connections; scaling past one node needs a pub/sub backplane (Redis Pub/Sub, NATS) so a message published on one node reaches a client connected to another.

Does the caller block waiting for a response, or fire a request and move on?

  • Does the caller need the result to proceed immediately, or can it continue and be notified later (callback, webhook, polling)?
  • Is the downstream operation fast and reliable, or slow/unreliable enough that blocking would tie up caller resources?
  • Does this operation need to be retried or queued if the downstream is temporarily unavailable?

Synchronous (request/response)

When to use: The caller genuinely needs the result before it can proceed — reading a user profile before rendering a page.

Trade-offs: Simple to reason about, but the caller is blocked (and its resources tied up) for the full duration of the downstream call.

Async via queue/message broker

When to use: Fire-and-continue work — sending an email, processing an uploaded video, updating a search index.

Trade-offs: Decouples caller from callee and smooths traffic spikes, but adds eventual-consistency and requires the caller to handle "not done yet" states.

Async via callback/webhook

When to use: Long-running third-party operations (payment processing, batch jobs) where polling would be wasteful.

Trade-offs: Efficient — no polling — but requires the caller to expose a reachable endpoint and handle the callback arriving out of order or more than once.

Go deeper

Async doesn’t remove the need for a response — it changes its shape

Even fire-and-forget work usually needs some way for the caller to check status later (a job ID, a webhook, a polling endpoint) — "async" is not the same as "the caller never finds out."

Should this run on a long-lived server process, or as short-lived, event-triggered functions?

  • Is there state that needs to persist between requests in-memory (favors server)?
  • Is load spiky/unpredictable (favors serverless's pay-per-invocation)?
  • Does the workload need to hold a long-lived connection (WebSockets, streaming) — most FaaS platforms cap execution time and don’t suit this well.

Traditional server

When to use: Steady load, long-lived connections, in-memory caching between requests.

Trade-offs: You manage scaling, and idle capacity costs money even at zero traffic.

Serverless / FaaS

When to use: Spiky/unpredictable load, event-driven workloads, wanting zero idle cost.

Trade-offs: Cold starts, execution time limits, no in-memory state between invocations.

Go deeper

Vendor-specific limits

Execution time caps and cold-start behavior vary significantly by provider (AWS Lambda vs. Vercel Functions vs. Cloudflare Workers) — the specific limits are a real implementation constraint but shouldn’t drive the initial server-vs-serverless call, which should be made on workload shape first.

Can any instance handle any request, or does a request need to land on the instance holding its state?

  • Is per-request state (session data, in-memory cache, an open connection) held on the instance itself, or externalized to a shared store?
  • If an instance dies mid-request, is that state recoverable elsewhere, or is it lost?
  • Does the load balancer need sticky sessions, or can it route purely round-robin?

Stateless

When to use: Any request can be served by any instance because all needed state lives externally (DB, cache, token).

Trade-offs: Trivially horizontally scalable and resilient to instance failure, but every piece of state needs an external home, which can add latency.

Stateful

When to use: In-memory game state, WebSocket connections, or anything where re-fetching state on every request would be too slow.

Trade-offs: Faster access to local state, but requires sticky routing and losing an instance means losing (or needing to recover) its state.

Go deeper

Externalizing state is the usual path from stateful to stateless

Moving session data into Redis, or connection state into a shared pub/sub layer, is the typical refactor that unlocks stateless horizontal scaling — worth naming explicitly rather than treating statelessness as a starting assumption.

Are you optimizing for how fast one request completes, or how many requests complete per unit time?

  • Does the user directly experience per-request latency (interactive UI), or only aggregate system throughput (batch pipeline)?
  • Is there room to batch requests together to improve throughput, and can the use case tolerate the added per-request latency that batching introduces?
  • What’s the actual SLA — a p99 latency target, or a requests-per-second target?
  • Is the link saturated (bandwidth-bound), or is it underused while requests wait on round trips (latency-bound)?

Optimize for latency

When to use: Interactive, user-facing requests — search-as-you-type, page loads, anything with a human waiting.

Trade-offs: Fast individual responses, often at the cost of lower overall throughput (less batching, less resource sharing).

Optimize for throughput

When to use: Batch processing, bulk ingestion, background jobs — nobody is watching a single request complete.

Trade-offs: Batching and queuing maximize total work done per second, but individual items wait longer before being processed.

Go deeper

These genuinely trade against each other, not just conceptually

Batching requests together (e.g. for a GPU inference call) raises throughput but adds queueing delay to each individual request — the right batch size is a direct latency/throughput dial, not a free win.

Bandwidth caps throughput, but it doesn’t set latency

Throughput can never exceed the narrowest link’s bandwidth, and that link only fills when enough data is in flight to cover it (bandwidth × round-trip time). More bandwidth shortens the transfer time for large payloads, but it can’t shorten the round trip itself. Cutting round trips (co-location, a CDN, fewer chatty calls) lowers latency without adding bandwidth. Name which of the two is the real limit before paying to fix it.

Should this result be computed live, in the request path, or precomputed ahead of time and served from a cached or materialized store?

  • Is the input space bounded and enumerable, or effectively unbounded/highly dynamic?
  • Does the computation depend on context that only exists at request time, or can it be fully determined ahead of the request?
  • If precomputed, how would staleness actually be detected and invalidated — is there a real trigger, or just a timer?

Live / on-demand computation

When to use: An unbounded or highly dynamic input space, or a result that depends on request-time context that can’t be known in advance.

Trade-offs: Always fresh, but every request pays the full compute and latency cost.

Precomputed / materialized

When to use: A bounded, enumerable input space, or an expensive computation worth amortizing — nightly aggregates, denormalized read models.

Trade-offs: Near-instant reads, but staleness is bounded by the last precompute run.

Hybrid: compute-then-cache

When to use: A large but skewed/long-tail input space, where a small hot set dominates traffic.

Trade-offs: Captures most of the benefit of both, but needs a real invalidation strategy or it silently serves stale results forever.

Go deeper

This is easy to confuse with three neighboring questions — worth disambiguating explicitly

sync-vs-async is about whether the CALLER waits for the response; real-time-vs-batch and batch-vs-streaming are about the DELIVERY CADENCE of an already-produced result; this question is about WHEN the underlying computation itself happens relative to the request. And unlike online-vs-batch-prediction (the ML-specific inference-serving-cadence question), this is the generalized version that applies to any expensive computation, not just model inference.

How do you protect a service from being overwhelmed by one client or a traffic spike?

  • Is the limit enforced per-client, or as one global ceiling across every caller?
  • Does legitimate traffic genuinely need short bursts above the sustained rate, or is a strictly even rate acceptable?
  • What identifies "a client" — IP address, authenticated user ID, or API key — and how reliable is that identifier (NAT/proxies flatten many real users behind one IP)?
  • When the limit is exceeded, does the caller get rejected outright (429 + Retry-After), or throttled/queued and served late?
  • Is this enforced by a single node, or does it need to hold consistent state across a horizontally-scaled fleet?

Token bucket

When to use: Need to allow short bursts up to a cap while still enforcing a steady average rate — the default choice for most public APIs (variants of this back AWS and Stripe rate limits).

Trade-offs: Cheap to check (compare token count, decrement), naturally burst-tolerant — but tuning bucket size and refill rate to match real traffic shape takes iteration, not a formula.

Leaky bucket

When to use: Need to smooth bursty input into a strictly constant output rate — protecting a fixed-capacity downstream (a worker pool, a legacy system) that can't absorb any burst at all.

Trade-offs: Guarantees a constant outflow regardless of how input arrives, but excess requests queue or drop rather than passing through — under sustained overload the queue itself becomes a memory/latency liability.

Fixed window counter

When to use: Simplicity matters more than precision — a quick global cap like "1000 requests per minute" behind a single counter.

Trade-offs: Trivial to implement (an atomic INCR + TTL in Redis), but allows up to ~2x the stated limit right at the window boundary — a burst at 11:59:59 and another at 12:00:00 both pass unpenalized.

Sliding window counter

When to use: Need the fixed window's simplicity without its boundary-burst flaw — most production rate limiters (Cloudflare, common API-gateway defaults) land here as the practical middle ground.

Trade-offs: Weights the previous window's count by how much of it still overlaps the current window, which closes the boundary gap at a fraction of the memory cost of a true sliding log (which stores every request timestamp for exact precision) — an approximation that's accurate enough for almost every real use case, not an exact count.

Go deeper

Where enforcement lives changes what it actually protects

Edge/CDN (Cloudflare, AWS WAF) stops abusive traffic before it costs you any compute at all — the cheapest place to block a true volumetric spike. API gateway (Kong, Envoy, NGINX) is the more common day-to-day layer, enforcing per-client/per-key policy in one place for every downstream service. Application-layer limiting sees the most business context (e.g. a single expensive endpoint deserves a tighter cap than the rest of the API), but by then the request has already paid for a network hop and a process to reject it.

A single node can count in memory; more than one node can't

Once traffic is spread across multiple app instances, an in-process counter only sees its own slice of requests — a client can blow past the intended limit just by landing on enough different nodes. Distributed rate limiting needs shared state (Redis INCR/EXPIRE, or a Lua script for atomicity) so every node is checking and incrementing the same counter, not five separate ones.

How do you evolve an API's contract without breaking the clients that are already depending on the old one?

  • How many external consumers depend on backward compatibility, and do you control their release cadence or not?
  • Is this a public API with unknown, uncoordinated consumers, or an internal API where a synchronized rollout is possible?
  • Is the change additive (a new optional field) or breaking (a renamed/removed field, changed semantics) — only breaking changes actually require a new version.
  • What's the deprecation window — how long does the old version stay live after the new one ships, and how is that communicated to consumers?
  • Does a client need to discover which versions exist and pick one, or is a single implicit default acceptable?

URI versioning (`/v1/users`)

When to use: Public APIs and anything where discoverability and cacheability matter most — the version is visible in every log line, browser address bar, and CDN cache key.

Trade-offs: Simplest to implement, test, and cache (each version is a genuinely distinct URL) — but it puts an implementation detail in the resource path, so "the same resource" now has multiple canonical URLs.

Header versioning (`Accept: application/vnd.app.v1+json`)

When to use: APIs that want the URL to stay a pure resource identifier and treat the version as metadata about the representation, not the resource's identity — common in stricter REST implementations.

Trade-offs: Keeps URLs clean and semantically correct, but is harder to test from a browser, less discoverable to a new consumer skimming the docs, and CDNs need explicit config to vary the cache by header instead of defaulting to URL.

Query-parameter versioning (`/users?version=1`)

When to use: A middle ground — version stays visible and easy to test in a browser like URI versioning, without minting a whole new path segment or route table per version.

Trade-offs: Easy to default (omit the param) and easy to test, but query params conventionally mean filtering/sorting, not identity — mixing the two is a recurring source of confusion, and caching by query string is less consistently supported than caching by path.

Go deeper

Only breaking changes need a new version at all

Adding an optional field or a new endpoint is backward-compatible and needs no version bump — versioning exists to protect consumers from changes they cannot safely ignore (removed/renamed fields, changed status codes, changed semantics). Treating every change as version-worthy fragments the API and multiplies the surface area you have to keep supporting.

A version shipped without a deprecation policy is a version that never gets retired

Shipping v2 without a committed sunset date for v1 means both have to be maintained indefinitely — the real ongoing cost isn't which versioning scheme you picked, it's the support burden of every version you've ever shipped and never turned off.

Category

Frontend

  • Where does rendering and interactive state actually need to live?
  • Is this the same for every visitor, or does it depend on who's asking?
  • Does the data this needs get fetched once, on the server, before anything renders — or does the client fetch (and possibly refetch) it after the page has already loaded?

Should this render as a Server Component, a Client Component, or be generated statically at build time — and where does interactive state actually need to live?

  • Does this component need hooks, event handlers, or a browser API (window, localStorage), or is it pure markup from server-known data?
  • Is the content identical for every visitor, or does it depend on per-request or per-user state?
  • How far up the tree does marking this a Client Component actually need to push — the whole page, or just the smallest interactive leaf?

Server Component (the App Router default)

When to use: Static or per-request server-rendered content with no client interactivity — e.g. most of this page’s own text sections.

Trade-offs: Ships zero client JS for that component, but no hooks/state/browser APIs are available inside it.

Client Component ('use client')

When to use: Needs state, effects, or a browser API — e.g. this site’s own useActiveSection scroll-spy hook, which is exactly why components like SweCompassSection.tsx must be client components.

Trade-offs: Ships JS and pays a hydration cost, so the boundary should be drawn at the smallest leaf component that actually needs it, not the whole page.

Static generation / ISR

When to use: Content is identical for every visitor with no personalization.

Trade-offs: Fastest possible response, but freshness is bounded by the last build or revalidation.

Go deeper

The decision compounds up the tree, not just at the component you’re editing

Pushing state up to a parent to avoid prop-drilling can silently convert a whole subtree from Server to Client Components — the fix isn’t always "lift state up," it can also be "push the client boundary down" to keep more of the tree server-rendered.

Category

Ops

  • Where does this run, on what shape of infrastructure, and how does it scale?
  • Once this is built, how does it actually get deployed into a live, running production environment — and what has to be verified before that's allowed to happen?
  • Once this is live, what's actually being logged, monitored, and alerted on — and who's watching it?
  • What's the specific, measurable threshold that separates 'healthy' from 'broken' — a technical metric like latency or error rate, or a business KPI — and do you actually know which one matters here?

Where does this actually run: self-hosted hardware, a native cloud provider, or a third-party PaaS/edge vendor?

  • What does this building block need — raw compute, managed storage, networking control? (roadmap.sh/system-design's own component taxonomy — DNS, CDN, load balancers, databases, caching, queues — is the "menu" to work through before picking an implementation.)
  • What's the team's own ops capacity — is there capacity to manage infrastructure, or does that need to be abstracted away?
  • What's the traffic/cost model — steady-state load favoring reserved capacity, or spiky/unpredictable load favoring pay-per-use?
  • Are there compliance requirements (GDPR, HIPAA, SOC2) that constrain where data can live or who can access the underlying hardware?
  • Is multi-region/global distribution a real near-term need, or premature?

Self-hosted / on-prem hardware

When to use: Strict data-residency/compliance requirements, extremely high sustained/predictable load where owning hardware amortizes below cloud rental, or an existing ops team with deep infra expertise.

Trade-offs: Full control, no vendor markup at scale, but capital cost, your team owns every failure mode, and no elastic burst capacity.

Native cloud (AWS/GCP/Azure)

When to use: Need the broadest managed-service catalog, multi-region reach, and elastic scaling without owning hardware.

Trade-offs: Huge surface area and flexibility, but real operational complexity (IAM, networking, cost governance) and a steeper learning curve.

Third-party PaaS/edge vendor (Vercel, Cloudflare)

When to use: Frontend-heavy or edge-latency-sensitive workloads, small teams that want to ship without an ops function at all.

Trade-offs: Fastest time-to-ship and near-zero ops overhead, but less control, potential vendor lock-in, and usage-based pricing that can surprise at scale.

Go deeper

Specific vendor selection within a hosting model

Once you know 'native cloud' is right, AWS vs. GCP vs. Azure is driven by factors outside pure technical merit: existing team expertise, negotiated enterprise pricing, which specific managed services you need (e.g. GCP's BigQuery vs. AWS Redshift for analytics), and compliance certifications already held by that provider for your industry.

This portfolio's own answer, as a real example

This very site runs a zero-cost hybrid: GitHub Actions as the compute/ETL layer, Vercel as a thin presentation layer, Hugging Face Spaces for GPU inference — see /projects for the full reasoning. That’s a concrete instance of "third-party PaaS" composed from multiple vendors rather than one, chosen specifically because the binding constraint was a $0 budget, not raw performance.

Is the bottleneck CPU cycles, or RAM and data movement?

  • When this workload is under load, is the CPU pegged, or is it waiting on memory/disk/network I/O?
  • Would adding more cores meaningfully speed this up, or is the working set simply too large to fit in fast memory?
  • Is the work parallelizable across cores, or inherently sequential?

Optimize for compute-bound

When to use: CPU is the pegged resource — heavy computation, encoding, ML inference on dense math.

Trade-offs: More/faster cores and parallelization help directly; but Amdahl’s law limits how much parallelizing a sequential portion actually buys.

Optimize for memory-bound

When to use: CPU is idle waiting on data — large working sets, cache misses, heavy data shuffling.

Trade-offs: More RAM, better caching, and reducing data movement (columnar formats, batching) help; adding CPU cores alone won’t.

Go deeper

Profile before optimizing

This distinction is only useful once measured (CPU utilization vs. memory bandwidth/cache-miss rate) — guessing which one is the bottleneck without profiling is a common wasted-effort trap.

Once this is deployed, how do you know it's healthy — and what actually happens when it isn't?

  • What gates code before it ships — tests, staged rollout, manual approval — versus what only observes it after it's already live?
  • Are the signals being watched infra-level (CPU, latency, error rate) or outcome-level? A service can be fully "up" by every infra metric while quietly returning wrong answers.
  • Who gets paged, is there a runbook, and what's the actual gap between time-to-detect and time-to-mitigate?
  • Is this component deterministic software or a served model — because for a model, "wrong" usually isn't a crash, it's a slow drift (see #drift-vs-staleness) that infra monitoring alone won't catch.

CI/CD gates + infra monitoring (logs/metrics/traces)

When to use: Deterministic services where correctness is binary and failures are loud — tests and validation catch regressions pre-deploy, dashboards and alerts catch outages post-deploy.

Trade-offs: Mature, well-understood tooling and fast MTTR for hard failures; blind to silent correctness decay, which is exactly the failure mode ML components are prone to.

SLOs + error budgets

When to use: Need one shared health contract that both engineering and product hold each other to, with an explicit, pre-agreed answer to how much risk can be spent shipping this week.

Trade-offs: Forces prioritization discipline; only works if the org actually halts shipping when the budget's blown, not just on paper.

Model/output observability (drift + quality monitoring)

When to use: Any component serving a model — infra can look perfectly healthy while prediction quality degrades under distribution shift.

Trade-offs: Catches what infra monitoring can't, but usually needs labeled ground truth or a proxy metric that lags reality — see #drift-vs-staleness for the staleness/detection-lag trade-off itself.

Go deeper

This question is domain-agnostic, but its concrete answer isn't

A stateless API and a served model need the same operational discipline — CI/CD, monitoring, on-call — but different failure signatures. The model needs the drift/quality layer in addition to standard infra monitoring, not instead of it.

Reference

The components of system design

The building blocks underneath any high-level design — framed here, with the genuinely multi-way forks worked as full decision cascades in The Questions below, and cross-linked to where the rest of the depth already lives on this page.

These five are now full decision cascades inside The Questions — Design and Data:

Caching

Trading freshness for speed at whichever layer is cheapest to add — L1 in-process, L2 shared (Redis), L3 CDN/edge — with a separate choice for how writes stay in sync.

Replication

Sync vs async copies of data across nodes — the durability/latency trade-off underneath most availability guarantees, and the reason read replicas can lag.

Vertical vs. horizontal scaling

A bigger machine vs. more machines — a fast, simple lever with a hard ceiling, vs. real horizontal capacity that compounds hard once a budget constraint is real.

API

REST, GraphQL, or gRPC as the contract between client and service — a protocol choice, distinct from the API-gateway traffic-management decision above.

Design principles every component above gets checked against

  • Keep it simple

    Start with the simplest design that satisfies the requirements, then layer in complexity only when a real constraint forces it.

    See High-Level Design →
  • Build for today's scale, with a runway

    Design for the load and requirements that actually exist today, simple enough to ship — but keep the next order of magnitude a lever to pull, not a rewrite. Know the runway a given design buys (how much growth it absorbs before a lever stops being enough) before treating the current shape as decided.

    See Scaling progression →
  • Design for failure

    Assume components will fail — redundancy and graceful degradation, not just a happy path.

  • Automate with monitoring & observability

    What isn't instrumented can't be operated — automation and visibility are the same discipline, not separate concerns.

  • Iterate and evolve

    Know when the current design has stopped fitting, and evolve it deliberately — architecture is ongoing judgment, not a one-time decision.

What Staff Looks Like

9 signals that separate staff-level answers

1

Quantifies cost explicitly

“"Adding a read replica costs us roughly $X/month at this instance size and buys us maybe 2x read capacity — versus caching, which is closer to $Y and could buy us 5-10x for our read pattern."”

A real number beats "it'll cost more" every time — it shows the trade-off was actually thought through, not gestured at.

2

Shows awareness of Conway’s Law

“"If we split this into services owned by separate teams, the service boundaries need to roughly match team boundaries, or we’ll end up with constant cross-team coordination overhead on every change."”

Org structure shapes system structure whether you plan for it or not — naming that trade-off explicitly is a senior-level instinct.

3

Designs operational concerns in, unprompted

“"I’d want alerting on p99 latency and error rate for this service, with an on-call runbook for the most likely failure mode — the queue backing up."”

A system that works in the design but has no monitoring/alerting story isn’t actually production-ready — say so before being asked.

4

Raises security considerations without being asked

“"This endpoint takes user-supplied input that gets stored and later rendered elsewhere — I’d want to validate/sanitize it here to avoid injection."”

Security-as-an-afterthought is a common junior-level gap — naming it proactively, even briefly, signals the opposite.

5

Proposes a phased rollout, not a big-bang rewrite

“"Rather than migrating everything at once, I’d strangler-fig this — route new traffic to the new system while the old one keeps serving existing traffic, then migrate incrementally."”

Big-bang migrations are high-risk and hard to roll back — a phased plan shows real production experience.

6

Uses data-driven thresholds, not vague "at scale" language

“"I wouldn’t shard until we’re consistently seeing write throughput above roughly X per second on the primary — before that, read replicas and caching should cover it."”

"At scale" is a placeholder, not an argument — naming the actual threshold is what makes the trade-off falsifiable.

7

Reasons about failure modes first

“"Before I go further — what happens when this service is unreachable? Does the caller retry, queue, or fail the whole request?"”

Designing the failure path before the happy path catches structural gaps the happy path alone would hide.

8

Shows build-vs-buy judgment

“"For rate limiting I’d reach for an existing managed solution rather than building a custom token-bucket implementation — this isn’t a differentiating problem for us to own."”

Knowing when NOT to build something custom is as much a signal of judgment as knowing how to build it.

9

Narrates the trade-off, not just the label

“"I'd make the like-count eventually consistent — a few seconds of staleness is invisible to a user scrolling a feed. I'd make the opposite call for an account balance, though: staleness there isn't cosmetic, it's a correctness bug someone notices immediately."”

Naming the trade-off and explaining why it fits THIS specific case is what separates real judgment from reciting "eventual consistency" as a keyword.

Senior (L5) vs. Staff+ (L6+) — architecture judgment

Scope of Impact
Senior

Implements the right components to fix a specific, stated scaling failure inside their own service or application.

Staff+

Standardizes architecture across a domain — the choices they make align technical direction with business strategy at the org level, not just one service's.

System Architecture
Senior

Picks a proven architecture for the stated requirements and can defend every piece of it under questioning.

Staff+

Names the next order of magnitude the current shape won't survive, and sequences the migration proactively — rather than over-designing for scale nobody has asked for yet.

Data & State Management
Senior

Picks the right database and consistency model for their own service's data.

Staff+

Reasons about which service is the system of record and how state propagates to consumers — a synchronous call, an event, or CDC — and what staleness each consumer can actually tolerate.

API & Decoupling
Senior

Builds a robust REST or GraphQL endpoint, and reaches for a message queue where async makes sense.

Staff+

Defines org-wide communication standards and drives decoupling specifically to prevent cascading failures across services, not just within one.

Ambiguity & Autonomy
Senior

Takes defined functional requirements and clarifies the non-functional constraints before building.

Staff+

Proactively identifies a future bottleneck and drives the architectural shift before growth forces it.

This is the technical-architecture slice of that shift only. The interpersonal and storytelling side — time horizon, autonomy, conflict, and the “local hero” trap — lives on the Behavioural prep page, not repeated here.

Common mistakes

  • Designing for infinite scale nobody asked for, instead of the scale actually stated in requirements
  • Chaining more than 2-3 synchronous service calls in a single request path without questioning the latency stack-up
  • Proposing dual-writes to two data stores without a reconciliation strategy — a classic split-brain risk
  • Retry logic with no backoff or jitter, which can turn a transient blip into a retry storm that takes the service down
  • Going silent while thinking for more than a few seconds — narrate, even a half-formed thought, rather than leaving dead air
  • Treating the first design as final rather than something to iterate on as new information comes in

Promotion checklist

  • Did I explicitly clarify non-functional requirements — latency, throughput, consistency — before jumping to implementation?
  • Can I articulate how this system survives failure, not just its happy path — redundancy, fault tolerance, and decoupling?
  • Did I justify the data-layer choice against actual read/write access patterns, rather than defaulting to whatever I know best?
  • Did I explain the caching or consistency trade-off in terms of what the business could actually tolerate, not just "eventual vs. strong"?
  • Did I factor CDN, DNS, and real network latency into how I reasoned about global scale?
  • When I pick an architecture, do I state the scale at which it stops working — not just that it works today?
Reference

The lifecycles underneath the lifecycle

This page's Design→Ops ordering isn't invented — it's one instance of a pattern that shows up, under different names, in every engineering discipline that ships something. Four of those named lifecycles, in their own real stages, then the pattern underneath all of them.

Software Development Lifecycle (SDLC)

The lifecycle any shipped software moves through, independent of what's inside it — this page's own Design→Ops stage ordering is the same shape, read as system-design decisions instead of process steps.

  1. 1

    Plan — Scope the problem and gather requirements before any design work starts.

  2. 2

    Design — Turn requirements into an architecture — the components and interfaces Build will implement.

  3. 3

    Build — Write the code that implements the design.

  4. 4

    Test — Verify the build against the requirements, functional and non-functional both.

  5. 5

    Deploy — Release the tested build into production.

  6. 6

    Maintain — Operate, patch, and evolve the shipped system — feeding back into the next Plan.

Source: AWS — "What is SDLC?"

Data Engineering Lifecycle

The lifecycle raw data moves through before it's trustworthy enough for a model or a dashboard to consume — the ML/MLOps Lifecycle below assumes this one is already built and automated.

  1. 1

    Generation — Data is produced at the source — application events, third-party APIs, user input.

  2. 2

    Storage — Landed somewhere durable and queryable, at whatever tier matches its access pattern.

  3. 3

    Ingestion — Moved from source into the pipeline, in batch or as a stream.

  4. 4

    Transformation — Cleaned, joined, and reshaped into the form downstream consumers actually need.

  5. 5

    Serving — Made available to whatever consumes it next — a dashboard, an application, or a model.

Security (continuous)Data management (continuous)DataOps (continuous)Data architecture (continuous)Orchestration (continuous)Software engineering (continuous)
Source: Reis & Housley — "Fundamentals of Data Engineering" (O'Reilly, 2022)

ML / MLOps Lifecycle

The Model stage's own loop, nested inside a standard CI/CD pipeline rather than running parallel to it — it assumes the Data Engineering Lifecycle's pipelines already exist.

  1. 1

    Data — Pull in the training data the Data Engineering Lifecycle already produced and served.

  2. 2

    Train — Fit the model against that data.

  3. 3

    Evaluate — Score the trained model against held-out data and the metric that actually matters for the product.

  4. 4

    Deploy — Ship the model behind the deployment risk ladder — shadow, canary, then full rollout.

  5. 5

    Monitor — Watch for drift and staleness once it's live, closing the loop back to Data when the world has moved on.

Source: Google Cloud — "MLOps: Continuous Delivery and Automation Pipelines in Machine Learning"

DevOps Lifecycle

The operational loop wrapped around everything above — an "infinite loop" because Monitor feeds directly back into the next Plan, not because it ever stops.

  1. 1

    Plan — Define the work — the same idea as SDLC's Plan, scoped to a single change.

  2. 2

    Code — Write and review the change.

  3. 3

    Build — Compile and package it into a deployable artifact.

  4. 4

    Test — Run automated tests against the artifact before it goes anywhere near production.

  5. 5

    Release — Mark the artifact ready — decoupled from Deploy so a release can be gated independently.

  6. 6

    Deploy — Push the artifact into production, via whichever deployment strategy fits the risk.

  7. 7

    Operate — Keep it running — the day-to-day of production ownership.

  8. 8

    Monitor — Watch it for the signal that starts the next Plan.

Source: Atlassian — "What is DevOps?"

The pattern underneath all 4

Strip the domain-specific vocabulary away and the same 6 steps recur — the mental model to reach for once the specific lifecycle's own naming stops mattering.

Define / scope

Understand what's being built and why, before committing to how.

SDLC: Plan

Data Eng: Generation & Storage

ML/MLOps: Data

DevOps: Plan

Build / transform

Turn the plan into the actual artifact — code, a transformed dataset, or a trained model.

SDLC: Design + Build

Data Eng: Ingestion + Transformation

ML/MLOps: Train

DevOps: Code + Build

Validate

Check the artifact against the requirement before it ships — non-negotiable even where it isn't named as its own stage.

SDLC: Test

Data Eng: — folded into the DataOps undercurrent, not a discrete stage

ML/MLOps: Evaluate

DevOps: Test

Ship / release

Make the artifact available to whatever consumes it next.

SDLC: Deploy

Data Eng: Serving

ML/MLOps: Deploy

DevOps: Release + Deploy

Operate / observe

Keep it running, and watch it — the step that turns "shipped" into "in production."

SDLC: Maintain

Data Eng: Security / Data management / DataOps / Data architecture / Orchestration / Software engineering undercurrents, continuously

ML/MLOps: Monitor

DevOps: Operate + Monitor

Learn / iterate

Feed what was learned back into the next Define/scope — none of these lifecycles are actually linear.

SDLC: — implicit: Maintain loops back to Plan

Data Eng: — not named as a discrete stage in the source material

ML/MLOps: — implicit: Monitor loops back to Data on drift

DevOps: — implicit: the "infinite loop" itself

None of the 4 lifecycles above name a discrete "Learn / iterate" stage — each loops back implicitly instead. The SWE Compass's Time axis is this site's own concrete version of that step made explicit — a dated, per-release record of what shipped, what broke, and what got learned, in that order.