System Design Foundations
Concurrency and Parallelism Basics
A checkout service takes 80 ms of CPU and 120 ms waiting on inventory, payments, and fraud. One request is mostly waiting. Ten thousand overlapping requests are a different machine: sockets, pools, and shared counters become the product.
is how you structure overlapping work so waiting does not stall everything else. is actually using extra cores, disks, or machines at the same instant. Interviews collapse when those two words are used as synonyms. This note keeps them apart, then applies Amdahl, Little's Law, and shared-state hazards to real request paths.
Checkout overlap
One request
Mostly I/O wait. Extra cores do nothing until you overlap other requests.
Keep thousands of checkouts progressing while each waits on a dependency.
Fraud scoring on eight cores only if the work splits and the serial join stays small.
Inventory, pay, and fraud run together; the slowest or failed leg decides the user.
What actually happens
A kitchen with one cook and four burners is concurrent: soups simmer while the cook chops. Two cooks at two stations is parallel. Software is the same. An event loop multiplexes sockets on one core. A thread pool on eight cores can both overlap waits and run CPU work at the same time—if you did not put a giant lock around the interesting data.
The hardware limit is real. One core cannot execute two instructions from two threads in the same cycle (ignoring SMT, which is a limited extra lane, not eight free cores). So when someone says “make it parallel,” ask: which independent hardware, which independent data, and what still has to happen in order?
At request scale, overlap is usually I/O. At cluster scale, overlap is shards. At a single hot counter, there is no overlap: that is a , and extra machines just queue on it.
How to choose the overlap model
1. Is the bottleneck waiting or computing? Waiting favors concurrent I/O (async or a modest blocking pool). Computing favors or a worker fleet.
2. Can the work be split without sharing writes? Independent shards parallelize. One global sequence number does not, no matter how many cores you buy.
3. Who owns the join? Fan-out without a deadline, a timeout budget, and compensation is how partial success becomes a support ticket.
Simple rule of thumb
Overlap waits; parallelize independent CPU or shard work; serialize the invariant. Size in-flight work with . Shrink the serial fraction before you scale the worker count.
One core interleaving vs many cores at once
Concurrent on one core
Read the diagram definition
sequenceDiagram
autonumber
participant C as One CPU core
participant K as Kernel scheduler
participant A as Checkout A
participant B as Checkout B
rect rgb(30, 41, 59)
Note over C,B: Concurrent, not parallel: one core interleaves
A->>C: Run until DB wait
C->>K: Block A, run B
B->>C: Run until cache wait
C->>K: Resume A
A->>C: Finish
end
Parallel fan-out on a request
Read the diagram definition
flowchart LR
API[Checkout API]
API --> INV[Inventory shard]
API --> PAY[Payments]
API --> FRD[Fraud]
INV --> JOIN[Join with deadline]
PAY --> JOIN
FRD --> JOIN
JOIN --> ACK[Commit or compensate]
Fan-out is . It only pays if inventory, payments, and fraud are independent enough to start together, and if the join has a deadline so one hung fraud call does not hold the checkout thread forever. Independent shards of a map-reduce job are . A video pipeline that demuxes, encodes, and muxes on different stages is .
Amdahl, Gustafson, and why more cores stall
is the interview hammer: speedup is limited by the part you cannot split. If a request is 20% serial (lock, unique-id mint, final commit) and 80% parallelizable, eight workers cannot give you 8×.
Amdahl (fixed problem size):
S(n) = 1 / (s + (1 - s) / n)
s = 0.20 serial, n = 8 cores
S = 1 / (0.20 + 0.80/8) = 1 / 0.30 ≈ 3.33×
Not 8×. The last 20% dominates.
As n → ∞, S → 1/s. Here the ceiling is 5×.
Gustson (scale the problem with the machine):
Scaled speedup ≈ n - (n - 1)s
Bigger jobs (more pixels, more shards) hide a fixed serial setup.
Design move:
Shrink s: remove global locks, partition the hot row,
mint IDs in batches, make the commit path short.| Serial fraction s | 8 cores | 32 cores | Ceiling 1/s |
|---|---|---|---|
| 5% | ~5.9× | ~12.3× | 20× |
| 20% | ~3.3× | ~4.4× | 5× |
| 50% | ~1.8× | ~1.9× | 2× |
Gustafson's observation is the other side: if you grow the problem (more users, more tiles, more partitions) as you add machines, a fixed serial setup becomes a smaller fraction of wall time. That is why adding Kafka partitions helps a growing ingest job, and why a single-row inventory counter never gets that gift.
Little's Law: how many things are in flight
L = λ × W
Checkout API:
λ = 2,000 requests/sec
W = 200 ms = 0.2 s in the service
L ≈ 400 requests in flight
If each in-flight request holds:
1 inbound connection + 3 outbound (inventory, pay, fraud)
≈ 1,600 sockets on this service alone
Pool math:
Need enough workers or async slots for L,
plus headroom for bursts, minus a hard cap
so a latency spike cannot create L = 2,000 × 2s = 4,000.Latency and concurrency are coupled. When W doubles under load, L doubles unless you shed traffic. That is why timeouts and load shedding are concurrency controls, not just UX. Threading models (the companion note) pick how those L slots are represented: threads, goroutines, or event-loop callbacks.
Shared memory versus message passing
| Style | Pays when | Fails when |
|---|---|---|
| Tight loops, in-process caches, tiny messages | Data races, false sharing, lock convoys, crash blast radius | |
| Isolation, distribution, explicit ownership | Copy cost, unbounded queues, lost messages, dual-write races |
System design interviews often pretend services only message-pass. Inside each service you still share heaps, connection pools, and rate-limit counters. Name both layers. A Redis INCR is message passing to a shard that itself uses shared memory and single-threaded command execution—an intentional serial that buys a simple mental model.
Races: memory-model bugs versus business bugs
A is two threads touching the same bytes without synchronization. A is a timing-dependent correctness hole even when each load and store is “safe.” Checkouts that both read stock=1 and both write stock=0 can be race conditions with no C++ data race if each access is atomic. Databases call the same shape lost update or write skew; that story lives in the transactions note. Here the point is: atomics do not equal invariants.
Data race
Unsynchronized read/write of a shared counter. Compilers and CPUs may tear values or reorder. Fix with atomics, mutexes, or confinement (only one thread owns the bytes).
Race condition
Check-then-act across two steps. Each step can be race-free. The pair is not. Fix with a single atomic step, a lock covering both, or a transaction.
Check-then-act (business race, even with a thread-safe map):
if (!reserved.contains(seat)) { // thread A and B both see empty
reserved.add(seat); // both add; last writer wins
charge(card); // maybe both charge
}
Atomic step (one critical section or one compare-and-set):
if (reserved.addIfAbsent(seat)) { // only one caller gets true
charge(card); // still need idempotency if charge retries
}False sharing and cache-line ping-pong
Parallel counters sitting on the same 64-byte cache line invalidate each other's cores on every increment. Throughput collapses even though the counters are logically independent. Padding, striping, or per-core counters with periodic merge are the usual fixes. This is why “just shard the metric” can still be slow inside one process, and why a single global QPS AtomicLong on a 32-core box can become the new serial fraction.
Striping a hot counter
Confinement beats sharing
Worked example: checkout latency budget
Sequential checkout: inventory 40 ms, payments 80 ms, fraud 60 ms, CPU 20 ms. Wall time 200 ms. Parallel fan-out of the three RPCs: wall time is CPU plus the slowest RPC, 20 + 80 = 100 ms, if you start them together and the join waits for all. You did not make inventory faster; you hid it under payments.
Sequential: 40 + 80 + 60 + 20 = 200 ms
Parallel: 20 + max(40, 80, 60) = 100 ms (2×)
Amdahl view of the original 200 ms:
serial CPU 20/200 = 10% if RPCs fully overlap
remaining serial is the join, timeouts, and commit
Failure design:
If fraud times out at 50 ms, do you fail closed,
fail open, or proceed with a pending review flag?
Parallelism made the happy path faster; it also
made partial failure the common case.Fan-out tax
Three parallel RPCs triple the chance that at least one is slow. Tail latency of the join is worse than any single dependency unless you hedge, shed, or degrade. Parallelism is not free latency.
Trade-offs to weigh
More in-flight work
Higher utilization and better wait overlap. Larger L means more memory, more sockets, worse p99 when a dependency slows, and a bigger blast if you retry.
More cores / shards
Helps only the parallel fraction. Coordination, skew, and a hot key can eat the gain. Extra machines also multiply connections to downstreams.
Shared memory speed
Lowest overhead inside one process. Highest risk of races and of one bad write corrupting everyone. Testing rarely reproduces the unlucky interleaving.
Message isolation
Clearer ownership and crash boundaries. You pay copies, queues, and dual-write problems. Unbounded mailboxes recreate the memory disaster shared heaps had.
Choosing and explaining concurrency vs parallelism
A complete explanation connects the mechanism to a requirement. Describe what improves when you introduce it, which new failure cases become possible, and how you would know that the trade-off is worthwhile.
Explain the core idea
Concurrency is overlapping progress; parallelism is simultaneous execution on extra hardware.
Name the serial fraction and apply Amdahl before promising linear scale-out.
Size in-flight work with Little’s Law, then cap it so latency spikes cannot explode memory.
When it belongs
A design fans out RPCs, shards a job, or claims “just add more threads.”
You need to separate I/O wait overlap from CPU speedup.
A hot counter, lock, or single partition is the real bottleneck.
How it fails
Global locks or hot keys make extra cores idle (Amdahl).
Unbounded concurrency turns a slow dependency into an OOM.
Fan-out without deadlines or compensation creates partial commits.
Check-then-act races survive even when individual loads are atomic.
Trade-offs to weigh
Utilization versus tail latency as in-flight work grows.
Shared-memory speed versus isolation of message passing.
Parallel fan-out latency versus higher chance of a slow straggler.
Common misunderstandings
Using concurrency and parallelism as synonyms.
Assuming eight cores give eight times throughput.
Confusing data races with business races.
Parallelizing a workflow that still joins on one row or one lock.
Wrap-up
Overlap the waits, split only the independent work, and keep the invariant in one short serial place. Cores, shards, and goroutines are tools for those two jobs—not decorations on an architecture diagram.
Exercises
- Amdahl. A job is 15% serial. What is the speedup at 16 workers? What serial work would you attack first in a log-compaction service?
- Little's Law. 800 QPS, 50 ms mean latency. How many requests are in flight? If p99 becomes 500 ms, what happens to L, and what cap do you set?
- Fan-out. Sketch inventory + pay + fraud with a 150 ms budget. Say what happens when fraud is late and when inventory succeeds but pay fails.
Primary sources
- Amdahl: Validity of the Single Processor Approach (AFIPS 1967) : serial-fraction ceiling on speedup.
- Sutter: The Free Lunch Is Over : why software must be concurrent after clock-rate stalls.
- Sutter / Pike discussions on concurrency vs parallelism : structure versus simultaneous execution.
Key takeaway
Concurrency keeps many tasks progressing; parallelism spends extra hardware on independent work. Serial fractions, hot keys, and unbounded in-flight requests decide whether more cores help or hurt. Design the overlap, then the join.