Trade-off Drills
Naming trade-offs in clear language is the skill that separates strong candidates. This drill gives you
the most common design decisions and trains you to articulate the trade-off both ways — because every real
decision is a trade-off, and “it depends… on what” is the mark of maturity. Answer each aloud in
trade-off vocabulary, then check.
Prerequisites: Trade-off Vocabulary, CAP & PACELC, Consistency Models
Time to work through: ~25 minutes, then revisit
The skill
🚨 There are no right answers in system design, only trade-offs — and the interview grades whether you can
name them. A weak candidate says “I’ll use NoSQL.” A strong one says “I’ll use NoSQL because the access
pattern is key-value and I need horizontal write scaling, accepting eventual consistency and losing joins —
if we needed transactions I’d switch to SQL.” Same choice, vastly different signal.
The template: “[Option A] gives [benefit] at the cost of [drawback]; [Option B] is the reverse. I’d pick
[X] here because [which side the requirements favor].”
Every drill below: articulate both sides and what tips the decision. Cover the answer first.
The foundational trade-offs
Strong vs eventual consistency
**Strong** guarantees every read sees the latest write — correct and intuitive, but costs latency and
availability (must coordinate, and per CAP sacrifices availability under partition). **Eventual** lets reads
see stale data briefly — fast, highly available, partition-tolerant, but the app must tolerate staleness and
conflicts. Pick **strong** for money, inventory, anything where stale = wrong (payments, stock). Pick
**eventual** for feeds, likes, view counts, caches where a second of staleness is harmless. The decision is:
*what does incorrectness cost here?* [Consistency Models](/system-design/01-foundations/13-consistency-models.html)
CAP: consistency vs availability under a partition
When the network partitions, you must choose: **CP** (refuse requests to stay consistent) or **AP** (stay
available, serve possibly-stale data). CP for systems where wrong data is unacceptable (payments, config, a
lock service); AP for systems where being down is worse than being slightly stale (feeds, shopping browse,
DNS). And per **PACELC**, even without a partition, you trade latency vs consistency. Name which your system
is and why. [CAP & PACELC](/system-design/01-foundations/14-cap-and-pacelc.html)
Latency vs throughput
**Latency** is per-request time; **throughput** is requests/sec. They can conflict: batching improves
throughput (amortize overhead) but adds latency (wait to fill a batch); tiny requests minimize latency but
waste throughput. Optimize latency for user-facing critical paths; throughput for background/bulk pipelines.
Batching, buffering, and pipelining trade one for the other. Know which your workload needs.
Data & storage decisions
SQL vs NoSQL
**SQL:** ACID, joins, complex queries, strong consistency, defined schema — at the cost of harder horizontal
scaling. **NoSQL:** horizontal scale, flexible schema, simple fast access patterns — at the cost of
(usually) eventual consistency, no joins, and modeling around specific queries. Tips the decision:
transactions + relational queries → SQL; massive scale + simple access + schema flexibility → NoSQL. Never
"NoSQL because scale" without naming the access pattern. [Choosing a Database](/system-design/03-data-and-storage/06-choosing-a-database.html)
Normalization vs denormalization
**Normalized:** no data duplication, easy consistent writes, but reads need joins (slower). **Denormalized:**
data duplicated/precomputed for fast reads (no joins), but writes must update multiple copies and risk
inconsistency. Normalize for write-heavy/consistency-critical relational data; denormalize for read-heavy
systems where read latency matters (feeds, analytics). It's the read-vs-write-cost trade.
Replication lag: sync vs async replication
**Synchronous** replication (wait for replicas before acking) → no data loss on primary failure, but slower
writes and reduced availability if a replica is down. **Asynchronous** (ack immediately, replicate after) →
fast writes, high availability, but risk losing recent writes on failure and replicas serve stale reads. Sync
when you can't lose a write (some financial); async for most systems, accepting the read-your-writes gap.
[Replication](/system-design/02-building-blocks/08-replication.html)
Architecture decisions
Monolith vs microservices
**Monolith:** simple to build/deploy/debug, fast for small teams, no network overhead — but hard to scale
teams and components independently, and one big deploy. **Microservices:** independent scaling/deployment,
team autonomy, fault isolation — at the cost of huge operational complexity, network latency, distributed-
system problems, and data consistency across services. 🚨 Start monolith; split to microservices when team/
scale pain justifies the complexity. Premature microservices is a classic anti-pattern. [Monolith vs Microservices](/system-design/05-architecture-patterns/01-monolith-vs-microservices.html)
Synchronous vs asynchronous communication
**Sync** (request/response, caller waits): simple, immediate result, easy to reason about — but couples
services (caller blocked, cascading failures) and limits throughput. **Async** (messages/events, fire-and-
forget): decoupled, resilient, absorbs bursts, scales — but adds complexity, eventual consistency, and
harder debugging/ordering. Sync when the caller needs the answer now and the callee is fast/reliable; async
for slow work, decoupling, and burst absorption. [Event-Driven Architecture](/system-design/05-architecture-patterns/03-event-driven-architecture.html)
Fan-out on write vs fan-out on read
**On write** (push to all followers' timelines when you post): fast reads (precomputed), but expensive
writes for high-fan-out users (the celebrity problem) and wasted work for inactive followers. **On read**
(gather at read time): cheap writes, but expensive reads. Tips it: read-heavy → write; but hybrid (write for
normal users, read for celebrities) resolves the extreme. [Design Twitter](/system-design/12-case-studies/08-twitter.html)
Delivery & processing decisions
At-most-once vs at-least-once vs exactly-once delivery
**At-most-once:** never duplicates, may lose messages — fast, for tolerable-loss data. **At-least-once:**
never loses, may duplicate — the common default, paired with idempotency to neutralize duplicates.
**Exactly-once:** ideal but impossible to truly guarantee end-to-end; approximated via at-least-once +
idempotency. Pick at-least-once + idempotent consumers for almost everything; at-most-once only when loss is
fine and duplicates are worse. [Idempotency](/system-design/04-distributed-systems/11-idempotency.html)
Batch vs stream processing
**Batch:** process large volumes periodically — high throughput, exact, simple, but high latency (results
are delayed). **Stream:** process events as they arrive — low latency, fresh, but harder (state, late/out-of-
order events) and often approximate. Batch for exact/historical (billing, reports); stream for real-time
(dashboards, alerts, fraud). Often both (Lambda/Kappa). [Batch vs Stream](/system-design/03-data-and-storage/10-batch-vs-stream.html)
Push vs pull
**Push:** server sends immediately → low latency, but can overwhelm slow consumers and needs connection
management. **Pull:** consumer fetches at its pace → natural backpressure, batching, slow-consumer-safe, but
adds polling latency/overhead. Push for real-time delivery (notifications, kill switches); pull for consumer-
paced processing (queue consumers, metrics scraping).
Reliability decisions
Fail-open vs fail-closed
When a dependency (auth check, rate limiter) is down: **fail-open** (allow the request) prioritizes
availability/UX but risks letting bad traffic through; **fail-closed** (block) prioritizes safety/security
but can cause an outage. Fail-open for non-critical user-facing limits; fail-closed for security/correctness-
critical checks (auth, payments). State which and why. [Resilience Patterns](/system-design/04-distributed-systems/13-resilience-patterns.html)
Scale up (vertical) vs scale out (horizontal)
**Scale up** (bigger machine): simple, no distribution complexity, but a hard ceiling and a single point of
failure — and it's how a matching engine or a single-writer system stays fast/deterministic. **Scale out**
(more machines): near-unlimited scale and redundancy, but distributed-systems complexity. Scale up first
(simpler) until you hit the ceiling or need redundancy; scale out for large/available systems. Note some
systems (stock exchange) deliberately scale up for determinism. [Scalability](/system-design/01-foundations/11-scalability.html)
🚨 For any decision, the strong answer is never “A” or “B” — it’s “it depends on [the specific factor], and
here it’s [X], so [A]. Practice completing these:
- “SQL or NoSQL? It depends on whether you need transactions and relational queries — here we handle
money, so SQL.”
- “Cache or not? It depends on the read/write ratio and staleness tolerance — here reads are 100× writes
and staleness is fine, so yes.”
- “Strong or eventual consistency? It depends on what stale data costs — here it’s a feed, so
eventual.”
- “Microservices or monolith? It depends on team size and independent-scaling needs — here it’s a small
team and early product, so monolith.”
Fill in the bracket for every decision. The bracket is the trade-off.
🛠️ Try it
1. Both-sides drill. For each <details> above, state the benefit and drawback of both options
without looking — then the deciding factor. If you can only argue one side, you don’t understand the
trade-off yet.
2. Devil’s advocate. Take any decision you’d make and argue the opposite choice convincingly. Being
able to defend both sides proves you understand the trade-off rather than parroting a default.
3. Trade-off narration in mocks. In every practice design, say the trade-off template aloud for each
decision: “X gives… at the cost of… I pick X because our requirements favor…” Make it a verbal habit until
it’s automatic.
Further reading