Tradeoffs
44 scenarios — canonical answer and when the opposite is right.
Sorted by interview frequency
You're building a banking ledger with multi-row transactions and complex reports across accounts, transactions, and customers.
Multi-row atomicity, consistency, isolation, durability (ACID) transactions + ad-hoc joins/reports are exactly what relational engines exist for. NoSQL forces app-side joins and either denormalization or distributed transactions.
If access patterns were a small fixed set keyed by account_id and you needed extreme write throughput, DynamoDB-style design would win.
A user uploads a video that must be transcoded into 5 resolutions before being playable.
Transcoding takes minutes — far exceeding any reasonable HTTP timeout. Queue + worker pool decouples upload from processing and allows retries.
For sub-second operations where the caller needs the result immediately, async adds complexity (polling, callbacks) without benefit.
Small team (8 engineers) building a new B2B SaaS product.
Microservices add operational complexity (deploy, observability, distributed tx) that an 8-person team rarely has capacity for. Keep boundaries clear in code; split later if scaling demands.
When team count grows to where independent deploys and language flexibility become bottlenecks, splitting along domain boundaries pays off.
Read-heavy workload where staleness for a few seconds is tolerable.
Cache-aside is simpler, the cache only stores what's actually read, and the app stays the source of truth. Time-to-live (TTL) handles staleness.
If reads must always be consistent with the latest write and write volume is moderate, write-through avoids the cache-miss latency spike after updates.
Multiple users may edit the same document, but concurrent edits are rare.
When conflicts are rare, optimistic locking avoids the throughput hit of held locks and gracefully surfaces conflicts to the user.
Hot rows with frequent contention (e.g., decrementing inventory in a flash sale) benefit from short pessimistic locks to avoid endless retry loops.
You need to fan out user events to ~10 downstream services and let each independently replay the last 7 days for backfill.
Kafka's append-only log + consumer groups + offset management is built for multi-subscriber fan-out with replay. SQS is a point-to-point queue; once consumed, messages are gone.
For a single worker fleet processing a simple work queue with no replay need, SQS is far simpler and cheaper.
You're designing internal service-to-service APIs (Application Programming Interfaces) across polyglot backends with strict latency service level agreements (SLAs) and schema evolution.
gRPC has lower latency (HTTP/2 + binary protobuf), enforced schemas via .proto, codegen for many languages, and built-in streaming.
For public-facing APIs consumed by browsers and third parties, REST/JSON is the dominant standard with better tooling and discoverability.
A collaborative editor needs sub-100ms updates across users.
WebSockets give true bidirectional push with low overhead. Long polling adds latency, reconnection cost, and load balancer headaches.
If updates are infrequent (every few minutes) and clients are behind hostile proxies, long polling is more reliable and simpler.
Single Postgres instance approaching CPU saturation as traffic grows.
Vertical is the simplest first step for relational DBs: no sharding, no app changes. Modern cloud DBs scale to enormous sizes.
Once you hit the largest instance available or write-throughput limits, horizontal (read replicas, sharding) becomes necessary.
Designing a fleet of API (Application Programming Interface) servers that need to scale horizontally based on traffic.
Stateless servers scale and recover trivially: any instance handles any request, autoscaling is straightforward, rolling deploys are safe.
Real-time apps (game servers, video conferencing) where state is heavy and latency matters often pin clients to a stateful node.
Sharding 100GB of session data across 10 cache nodes, with occasional node additions/removals.
Modulo remaps ~all keys when N changes. Consistent hashing moves only ~1/N of keys, preventing cache stampedes during scaling.
If the node count is truly fixed forever, modulo is simpler and has perfectly uniform distribution.
Mobile clients on flaky networks need to fetch a screen's worth of nested, related data in one round trip.
GraphQL lets the client request exactly the fields it needs in one query, eliminating waterfall round trips and over-fetching on slow networks.
When response shapes are stable, caching matters (HTTP cache), and team familiarity favors REST, GraphQL's complexity (N+1, auth, query cost) isn't worth it.
Analytics dashboard scanning aggregations over billions of rows but only 3-4 columns at a time.
Columnar stores read only the queried columns, compress them extremely well, and vectorize aggregations — orders of magnitude faster for analytics.
For online transaction processing (OLTP) workloads with frequent row inserts/updates and lookups by primary key, row stores win on write latency and point reads.
Write-heavy app deployed to two regions for low-latency writes in both.
Single leader forces one region's writes to cross-region — defeating the latency goal. Multi-leader allows local writes; you accept conflict resolution complexity.
If consistency outweighs latency (e.g., financial ledger), keep single leader and accept the cross-region write hit.
An order checkout flow spans payment, inventory, and shipping microservices.
Two-phase commit (2PC) blocks on participant failure and is rarely supported across modern services. Sagas embrace eventual consistency with explicit compensation steps.
If all participants share one DB engine that supports XA and you need strict atomicity, two-phase commit (2PC) remains an option.
You need a cache that also supports rate-limit counters, sorted sets for leaderboards, and pub/sub for notifications.
Redis ships rich data structures (sorted sets, hashes, streams) and pub/sub. Memcached is purely string key-value (KV).
If you genuinely only need a least recently used (LRU) string cache and want maximal memory efficiency + simplicity, Memcached has lower overhead.
Public blog/marketing site that needs SEO and fast first-paint on mobile.
SSR/SSG ships rendered HTML for instant first paint and reliable SEO. Pure SPAs delay both behind a JS bundle download + execution.
Highly interactive logged-in apps (dashboards, editors) where SEO doesn't matter often prefer SPAs for richer client-side state.
Real-time multiplayer game where player positions update 60×/sec.
Stale position packets are useless — you want the newest, not retransmits of old ones. UDP (User Datagram Protocol) avoids head-of-line blocking from packet loss.
For chat, file transfer, or game inventory updates where every byte must arrive in order, TCP (Transmission Control Protocol) reliability is required.
User requests account deletion in a system that powers downstream analytics.
GDPR/privacy expectations require actual removal of personal data. Soft delete keeps the data, which becomes a liability.
For business documents (invoices, audit records) where you must retain history, soft delete with role-based visibility is correct.
Mid-size startup with shared libraries across 12 services, frequent cross-cutting changes.
Atomic cross-service changes, single source of truth for shared libs, unified tooling. Frequent cross-cutting changes are painful across polyrepos.
Independent teams shipping totally decoupled services (especially with different release cadences or open-source components) often prefer polyrepo.
A hot counter is hit by 64 threads thousands of times per second. You need correctness without crushing throughput.
For single-word updates, CAS (Compare-And-Swap) avoids the syscall and context-switch cost of mutex contention and scales linearly with CPU count up to the point where CAS retries dominate. Mutexes win once the critical section grows to multiple statements.
If the critical section is several reads and writes that must be atomic together, a mutex is simpler and faster than a hand-rolled CAS loop.
A service makes ~5 downstream HTTP (Hypertext Transfer Protocol) calls per request, 80% of request time is waiting on I/O, and you expect 50k concurrent connections.
I/O-bound workloads with high concurrency don't need a thread per in-flight call — the threads just sleep on epoll. An event loop drives tens of thousands of concurrent I/Os with a handful of OS threads, cutting memory and context-switch overhead.
For CPU-bound work, threads (one per core) win — async buys nothing when nobody is sleeping.
You're locking a 1M-entry in-memory map. One big lock is simple but throughput is poor under writes.
Striped / fine-grained locks let unrelated keys be updated concurrently — under uniform keys you get roughly N-way write parallelism. This is exactly how Java's pre-Java-8 `ConcurrentHashMap` worked.
If operations frequently touch many keys (`putAll`, atomic multi-key updates) a single lock is simpler and avoids deadlock from acquiring multiple stripes.
Multiple users edit the same record but conflicts are rare. You're choosing the concurrency-control model.
When conflicts are rare, optimistic concurrency lets writers proceed without blocking and only retries the unlucky few. Pessimistic locks serialize everyone and tie up resources for the slow human-shaped edits.
Hot rows where conflicts are the norm — optimistic devolves into a retry storm and pessimistic is cheaper.
Your service issues many DB and HTTP (Hypertext Transfer Protocol) calls per request. You're choosing the I/O style for the platform.
Non-blocking I/O scales connection count and concurrency far past what threads can — Netty / Tokio / asyncio routinely drive 100k connections per box. Threads cost ~1MB of stack each and context-switch overhead grows with the pool.
Simple internal tools or batch jobs with low concurrency — sync code is dramatically easier to reason about and debug. The cognitive tax of async isn't worth it under low load.
Shopping cart service for a global e-commerce site. Users add items from mobile, web, and tablet; some sessions are offline.
Cart UX tolerates 'union of all adds' semantics, and users hate write failures. The original Dynamo paper picked exactly this trade for Amazon's cart. CRDTs make merges deterministic.
For an inventory decrement or a bank balance, strong consistency wins: silently merging two debits is worse than rejecting one.
Primary Postgres in us-east-1 replicating to a us-west-2 standby for DR. Writes are user-facing checkout transactions.
Cross-region sync replication adds ~70ms to every commit, killing checkout latency. Async accepts a small RPO (recovery point objective) window of unreplicated writes for big latency wins.
Financial ledgers with zero-data-loss requirements pick sync (or quorum sync to 2 of 3 standbys). Spanner's TrueTime-based sync replication is the gold standard.
Write-heavy time-series workload, 100k writes/sec, can tolerate eventual reads, must stay available during single-AZ outages.
Leaderless gives write availability during partitions and scales writes horizontally without re-electing a leader. Quorum tuning controls consistency per op.
If writes must be linearizable and you need cross-row transactions, leader-based (or Spanner-style sharded Paxos) is the right pick.
Order workflow spans 5 microservices (cart, payment, inventory, shipping, notifications), each with its own database.
2PC across independently-deployed services with separate teams is operationally toxic: long-held locks, blocking on coordinator, and cross-team schema coupling. Sagas keep services autonomous.
Inside one trusted boundary (Spanner shards, MySQL XA in one app) 2PC is fine and gives true atomicity. Sagas trade that for eventual consistency.
A user-profile service with N=3 replicas per key, light contention, multi-region.
Quorum survives a replica down without failover; single-master needs leader election (seconds of unavailability) on every primary loss. For multi-region, quorum sidesteps the WAN-pinned leader.
If you need linearizable reads and cross-key transactions, a single master (or Raft group) is much simpler than gluing quorum + version vectors + repair.
Tracking which of ~1500 worker nodes are alive in a batch-processing cluster.
Raft membership churn at 1500 nodes saturates the leader; you do not need linearizable liveness, just convergence within seconds. SWIM gives sub-second failure detection at constant per-node cost.
For a small, security-critical control plane (10s of nodes) where 'who is in the cluster' must be strongly consistent and auditable, Raft membership wins.
Storage engine for a write-heavy time-series ingest service. Reads are mostly recent data; durability matters; SSDs.
LSM converts random writes into sequential SSTable flushes plus background compaction, which is a much better fit for write-heavy ingest. Reads of recent data hit the memtable and L0, so read amplification stays modest.
Mixed OLTP with heavy in-place updates to the same rows and strict read latency SLOs favor B-trees. LSM read amp and compaction stalls become liabilities.
User-profile service joins users, preferences, and subscription rows on almost every read. Writes are infrequent.
Read-heavy, write-light workloads pay the denormalization cost (extra storage, update fan-out) only on rare writes, and amortize it across many cheap single-row reads. Joins disappear from the hot path.
Write-heavy systems with many places to update on a single change (think e-commerce inventory) usually keep normalization to avoid update anomalies and write amplification.
OLTP service mostly does single-row updates with the occasional multi-row consistency check (e.g., 'don't double-book a seat').
READ COMMITTED is the cheapest correct default for the common case. Pay for SERIALIZABLE (or explicit row locks / unique constraints) only on the few transactions that actually need protection from write skew or phantoms.
If multi-row invariants are pervasive (financial postings, scheduling), running everything at SERIALIZABLE (Postgres SSI) is simpler than reasoning about which transactions need SELECT FOR UPDATE.
A reporting team wants to run hour-long aggregate queries against the same Postgres serving your checkout flow.
OLTP engines are tuned for short row-oriented transactions; long scans contend with the buffer cache, pollute the plan cache, and (under MVCC) hold back the xmin horizon, bloating tables. OLAP stores are columnar, vectorized, and built for this.
Small scale or early-stage products can absolutely run reports on a read replica; introducing a warehouse before you need one is over-engineering.
Choosing between Postgres-style heap tables and MySQL/InnoDB-style index-organized tables (clustered on PK) for a new schema.
When most reads are by PK or PK-range, clustering rows in PK order makes the leaf pages the rows. Range scans are sequential and cache-friendly, and the PK index has zero extra indirection.
Workloads dominated by secondary-index lookups suffer in IOT, because secondary indexes store the PK (not a row pointer) and need a second lookup. Heap tables with multiple covering secondary indexes win there.
Postgres `synchronous_commit`: leave it ON (fsync WAL before ack) or set it OFF (ack early, fsync later) to chase throughput.
Default ON is the only setting that guarantees a committed transaction survives a crash. The right way to claw back throughput is batching (group commit), faster storage, or SSDs with battery-backed cache, not silently losing the last N transactions.
Workloads where the last few seconds of writes are truly disposable (telemetry, analytics ingest with replay) can flip it off for big throughput gains. Be explicit about the data-loss window.
Serving a global audience over flaky mobile networks. Pick HTTP (Hypertext Transfer Protocol) 2 or HTTP/3 at the edge.
QUIC streams are independent at the transport layer, so a single dropped packet does not stall every concurrent stream like it does on TCP (Transmission Control Protocol). QUIC also has 0-RTT resumption and connection migration across IP changes (Wi-Fi to LTE).
Inside the data center on reliable low-loss links, HTTP/2 over TCP is simpler, more debuggable, and friendlier to middleboxes. UDP (User Datagram Protocol) is also more likely to be rate-limited by corporate firewalls.
Returning clients are hitting your API (Application Programming Interface). Do you allow TLS (Transport Layer Security) 1.3 0-RTT or require a full 1-RTT handshake?
0-RTT early data is replayable by an attacker that captures it. For mutations, payments, or anything non-idempotent, that is unsafe. The 1 extra round trip is worth the safety.
For idempotent GETs on a latency-critical read path (search, feed, static APIs), enable 0-RTT and bound the replay window with an anti-replay cache.
Server expected to hold 100k concurrent long-lived connections. Thread-per-connection or event loop?
Threads cost ~1MB of stack and a kernel-scheduled context each; 100k threads is 100GB of stack and ruinous context-switch overhead. Event loops watch many fds from one (or N=cores) threads with O(1) ready notification.
For short CPU-heavy requests with few concurrent connections, thread-per-request is easier to reason about and lets blocking calls (sync DB drivers, CPU work) stay on their own thread. Java's virtual threads (Loom) blur the line further.
Running untrusted plugin code or risky native libraries. Process or thread?
Processes have separate address spaces; a segfault, memory corruption, or OOM kills only that process. Threads share memory, so one bad pointer takes down the whole server. Chrome and modern browsers run each tab in its own process for this reason.
If you need cheap shared memory and frequent communication (e.g., a worker pool over a hot in-memory cache), threads avoid IPC overhead and fork cost. Trust boundary matters more than performance.
Database is doing its own page cache and you do not want the kernel double-buffering. Buffered or direct I/O?
When the app already manages its own buffer pool (Postgres, MySQL, RocksDB, ScyllaDB), the kernel page cache is wasted RAM and double work. O_DIRECT bypasses it and gives the DB deterministic control over what is cached.
For file servers, log shippers, or apps that benefit from OS read-ahead and free shared caching across processes, buffered I/O is faster and simpler. O_DIRECT requires aligned buffers and is unforgiving.
Load balancing across backends where request durations vary wildly (some 1ms, some 5s). Round robin or least connections?
Round robin assumes equal-cost requests. With heavy-tailed durations it piles slow requests on whichever backend got unlucky. Least-connections routes new work to the least-loaded backend, smoothing tail latency.
For uniform, short, stateless requests with sticky-session needs, round robin (or consistent hashing) is cheaper and avoids the cross-LB connection-count bookkeeping. Least-connections also gets tricky across multiple LB instances.