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.

ABest pick
PostgreSQL (SQL)
B
DynamoDB (Key-Value NoSQL)
Why PostgreSQL (SQL) wins

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.

When the opposite is right

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.

A
Process synchronously in the request handler
BBest pick
Enqueue and process asynchronously
Why Enqueue and process asynchronously wins

Transcoding takes minutes — far exceeding any reasonable HTTP timeout. Queue + worker pool decouples upload from processing and allows retries.

When the opposite is right

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.

ABest pick
Modular monolith
B
Microservices
Why Modular monolith wins

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 the opposite is right

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.

ABest pick
Cache-aside
B
Write-through cache
Why Cache-aside wins

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.

When the opposite is right

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.

ABest pick
Optimistic locking (version/etag)
B
Pessimistic locking (DB row lock)
Why Optimistic locking (version/etag) wins

When conflicts are rare, optimistic locking avoids the throughput hit of held locks and gracefully surfaces conflicts to the user.

When the opposite is right

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.

ABest pick
Kafka
B
AWS SQS
Why Kafka wins

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.

When the opposite is right

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.

A
REST + JSON
BBest pick
gRPC + Protobuf
Why gRPC + Protobuf wins

gRPC has lower latency (HTTP/2 + binary protobuf), enforced schemas via .proto, codegen for many languages, and built-in streaming.

When the opposite is right

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.

A
Long polling
BBest pick
WebSockets
Why WebSockets wins

WebSockets give true bidirectional push with low overhead. Long polling adds latency, reconnection cost, and load balancer headaches.

When the opposite is right

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.

ABest pick
Scale vertically (bigger box)
B
Scale horizontally (more boxes)
Why Scale vertically (bigger box) wins

Vertical is the simplest first step for relational DBs: no sharding, no app changes. Modern cloud DBs scale to enormous sizes.

When the opposite is right

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.

ABest pick
Stateless servers + external store
B
Stateful servers with sticky sessions
Why Stateless servers + external store wins

Stateless servers scale and recover trivially: any instance handles any request, autoscaling is straightforward, rolling deploys are safe.

When the opposite is right

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.

A
hash(key) % N
BBest pick
Consistent hashing
Why Consistent hashing wins

Modulo remaps ~all keys when N changes. Consistent hashing moves only ~1/N of keys, preventing cache stampedes during scaling.

When the opposite is right

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.

A
REST
BBest pick
GraphQL
Why GraphQL wins

GraphQL lets the client request exactly the fields it needs in one query, eliminating waterfall round trips and over-fetching on slow networks.

When the opposite is right

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.

A
Row-oriented (Postgres)
BBest pick
Columnar (BigQuery/ClickHouse)
Why Columnar (BigQuery/ClickHouse) wins

Columnar stores read only the queried columns, compress them extremely well, and vectorize aggregations — orders of magnitude faster for analytics.

When the opposite is right

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.

A
Single leader with cross-region replicas
BBest pick
Multi-leader (active-active)
Why Multi-leader (active-active) wins

Single leader forces one region's writes to cross-region — defeating the latency goal. Multi-leader allows local writes; you accept conflict resolution complexity.

When the opposite is right

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.

A
Two-phase commit
BBest pick
Saga (orchestrated compensations)
Why Saga (orchestrated compensations) wins

Two-phase commit (2PC) blocks on participant failure and is rarely supported across modern services. Sagas embrace eventual consistency with explicit compensation steps.

When the opposite is right

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.

A
Memcached
BBest pick
Redis
Why Redis wins

Redis ships rich data structures (sorted sets, hashes, streams) and pub/sub. Memcached is purely string key-value (KV).

When the opposite is right

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.

A
Client-side SPA
BBest pick
Server-side rendering (or SSG)
Why Server-side rendering (or SSG) wins

SSR/SSG ships rendered HTML for instant first paint and reliable SEO. Pure SPAs delay both behind a JS bundle download + execution.

When the opposite is right

Highly interactive logged-in apps (dashboards, editors) where SEO doesn't matter often prefer SPAs for richer client-side state.

Related:SD: CDN

Real-time multiplayer game where player positions update 60×/sec.

A
TCP
BBest pick
UDP
Why UDP wins

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.

When the opposite is right

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.

ABest pick
Hard delete row
B
Soft delete (deleted_at flag)
Why Hard delete row wins

GDPR/privacy expectations require actual removal of personal data. Soft delete keeps the data, which becomes a liability.

When the opposite is right

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.

ABest pick
Monorepo
B
Polyrepo (one per service)
Why Monorepo wins

Atomic cross-service changes, single source of truth for shared libs, unified tooling. Frequent cross-cutting changes are painful across polyrepos.

When the opposite is right

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.

A
Guard the counter with a mutex (`ReentrantLock`)
BBest pick
Use a lock-free `AtomicLong` with CAS
Why Use a lock-free `AtomicLong` with CAS wins

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.

When the opposite is right

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.

A
Thread-per-request with a large pool
BBest pick
Async / event-loop with futures
Why Async / event-loop with futures wins

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.

When the opposite is right

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.

A
One global lock around the whole map
BBest pick
Striped locks (e.g., 16 segments, each with its own lock)
Why Striped locks (e.g., 16 segments, each with its own lock) wins

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.

When the opposite is right

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.

A
Pessimistic: take a row lock for the duration of the edit
BBest pick
Optimistic: read with version, update conditional on version
Why Optimistic: read with version, update conditional on version wins

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.

When the opposite is right

Hot rows where conflicts are the norm — optimistic devolves into a retry storm and pessimistic is cheaper.

You're building a stateful service with many internal components mutating shared state. You want to keep it correct as it grows.

A
Shared memory + locks across components
BBest pick
Actors / message-passing — each component owns its state
Why Actors / message-passing — each component owns its state wins

Message passing eliminates whole categories of bugs (deadlock, race, torn read) by removing sharing — each actor is single-threaded inside. Scales to distributed systems trivially because the model is location-transparent.

When the opposite is right

Tight, low-latency loops over shared in-memory state (an in-process cache, a matching engine) — locks plus careful design beat message-copy overhead.

Your service issues many DB and HTTP (Hypertext Transfer Protocol) calls per request. You're choosing the I/O style for the platform.

A
Synchronous I/O with a large thread pool
BBest pick
Asynchronous non-blocking I/O with futures/await
Why Asynchronous non-blocking I/O with futures/await wins

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.

When the opposite is right

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.

A
Strong consistency (single-region Raft)
BBest pick
Eventual consistency (multi-master + CRDT merge)
Why Eventual consistency (multi-master + CRDT merge) wins

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.

When the opposite is right

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.

A
Synchronous replication (commit waits for standby ack)
BBest pick
Asynchronous replication (commit on primary, replicate after)
Why Asynchronous replication (commit on primary, replicate after) wins

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.

When the opposite is right

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.

A
Leader-follower (Postgres, MySQL, Raft KV)
BBest pick
Leaderless (Cassandra, DynamoDB, Riak)
Why Leaderless (Cassandra, DynamoDB, Riak) wins

Leaderless gives write availability during partitions and scales writes horizontally without re-electing a leader. Quorum tuning controls consistency per op.

When the opposite is right

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.

A
Two-phase commit across all 5 services
BBest pick
Saga with compensating actions
Why Saga with compensating actions wins

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.

When the opposite is right

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.

ABest pick
Quorum writes (W=2)
B
Single-master writes
Why Quorum writes (W=2) wins

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.

When the opposite is right

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.

A
Raft consensus over membership list
BBest pick
Gossip / SWIM-based membership
Why Gossip / SWIM-based membership wins

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.

When the opposite is right

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.

A
B-tree / B+tree storage (Postgres, InnoDB style)
BBest pick
LSM tree (RocksDB, Cassandra style)
Why LSM tree (RocksDB, Cassandra style) wins

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.

When the opposite is right

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.

A
Normalized schema with joins at read time
BBest pick
Denormalized profile document
Why Denormalized profile document wins

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.

When the opposite is right

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').

ABest pick
READ COMMITTED isolation
B
SERIALIZABLE isolation
Why READ COMMITTED isolation wins

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.

When the opposite is right

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.

A
Run analytics on the OLTP database directly
BBest pick
Move analytics to a separate OLAP store (columnar warehouse, replica with column store extension, Snowflake/BigQuery)
Why Move analytics to a separate OLAP store (columnar warehouse, replica with column store extension, Snowflake/BigQuery) wins

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.

When the opposite is right

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.

A
Heap-organized table with separate indexes
BBest pick
Index-organized table clustered by primary key
Why Index-organized table clustered by primary key wins

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.

When the opposite is right

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.

ABest pick
synchronous_commit = on (fsync before ack)
B
synchronous_commit = off (ack before fsync)
Why synchronous_commit = on (fsync before ack) wins

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.

When the opposite is right

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.

A
HTTP/2 over TCP
BBest pick
HTTP/3 over QUIC
Why HTTP/3 over QUIC wins

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).

When the opposite is right

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?

A
Allow 0-RTT for resumed sessions
BBest pick
Require 1-RTT (no early data)
Why Require 1-RTT (no early data) wins

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.

When the opposite is right

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?

A
Thread-per-connection
BBest pick
Event loop on epoll / kqueue
Why Event loop on epoll / kqueue wins

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.

When the opposite is right

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?

ABest pick
Separate process
B
Separate thread in the same process
Why Separate process wins

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.

When the opposite is right

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?

A
Buffered I/O (page cache)
BBest pick
Direct I/O (O_DIRECT)
Why Direct I/O (O_DIRECT) wins

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.

When the opposite is right

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?

A
Round robin
BBest pick
Least connections
Why Least connections wins

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.

When the opposite is right

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.