Backend / Distributed Systems & Scalability

Distributed Systems & Backend Patterns, End to End

50+ patterns that keep production systems alive under failure, load, and scale — resilience, load management, caching, messaging, databases, distributed coordination, traffic routing, reliability, scalability, security, and observability. Every pattern with the problem it solves, a diagram, real code, and the trade-off nobody puts on the slide.

On this page

Every pattern below exists to answer one question: what happens when the thing you're depending on doesn't work the way you assumed? A dependency gets slow. A network partitions. A cache goes cold at the worst possible moment. Ten thousand requests arrive in the same second. Each pattern is a specific, named answer to one of those moments — and the patterns compose: a real production system runs a dozen of these simultaneously, layered on top of each other.

This guide groups them the way they actually get reached for — by the failure or bottleneck each one is defending against — then closes with a cheat sheet mapping symptom to pattern.

Category 1 — Resilience Patterns

§1Resilience: surviving a dependency that's failing

Resilience patterns share one premise: something you depend on will fail, and it will fail in the middle of your request. The goal isn't preventing that failure — you usually can't — it's making sure one failing dependency doesn't take your whole system down with it.

Exponential Backoff

Solves: retry storms — a fleet of clients all retrying a failed call at the same instant, which is often what tips a struggling service over into an outage.

The idea is simple: each retry waits longer than the last, growing exponentially, so a client doesn't hammer a struggling service at a fixed interval. delay = base * 2^attempt. Without jitter, every client computes the exact same delay sequence and they all retry in lockstep anyway — jitter (a random offset added to each delay) is what actually breaks the synchronization.

Fig. 1

Exponential backoff with jitter vs without

Exponential backoff with jitter vs withoutTwo timelines of the same clients retrying a failed call. Without jitter every client retries at t=1s, 2s, 4s and 8s, producing the same spike just delayed. With jitter each client adds a random offset, so the same retries spread out over time and the service recovers.Client 1Client 2Client NClient 1Client 2Client Nt=0st=1st=2st=4st=8stimet=0st=1st=2st=4st=8stimeWithout jitterdelay = base * 2^attemptall N clients retry at t=1s, t=2s, t=4s, t=8s — same spike, just delayedWith jitter+ random offset per clientsame N clients, randomized delay per client — load spreads over time
Same number of retries, same base delay — jitter is the only difference, and it's the difference between a self-inflicted second outage and a quiet recovery.
def backoff_delay(attempt, base=1.0, cap=30.0):
    exp = min(cap, base * (2 ** attempt))
    return random.uniform(0, exp)   # "full jitter" -- AWS's recommended formula

for attempt in range(max_retries):
    try:
        return call_service()
    except TransientError:
        time.sleep(backoff_delay(attempt))
raise MaxRetriesExceeded()
Full jitter vs. equal jitter
"Full jitter" (random(0, cap)) gives the best spread but means some retries fire almost immediately. "Equal jitter" (cap/2 + random(0, cap/2)) guarantees a minimum wait at the cost of a less even spread. AWS's own architecture blog recommends full jitter for most cases — the minor risk of an early retry is worth the better load distribution.

Circuit Breaker

Solves: cascading failures — a slow or dead downstream service causing every upstream caller to pile up threads waiting on it, until the caller itself runs out of resources and fails too.

Modeled directly on the electrical version: too many failures and the circuit "trips," and calls fail immediately without ever touching the struggling service — giving it room to recover instead of being hit by requests while it's already down.

Fig. 2

Circuit breaker's three states

Circuit breaker's three statesCLOSED passes calls through. When the failure rate exceeds a threshold it trips to OPEN, where calls fail instantly. After a timeout elapses it moves to HALF-OPEN, which allows one test call: success returns to CLOSED, failure goes straight back to OPEN.failure rate > thresholdtimeout elapsestest call fails →straight back to OPENtest call succeeds →back to CLOSEDCLOSEDcalls pass through1OPENcalls fail instantly2HALF-OPEN1 test call allowed3HALF-OPEN is the whole pointit's how the breaker finds out the dependency recovered without ever letting a full flood of traffic hit it to check
Three states, and HALF-OPEN allows exactly one (or a small handful of) probe calls — never the full traffic volume — to test recovery.
class CircuitBreaker:
    def __init__(self, failure_threshold=5, reset_timeout=30):
        self.state = "CLOSED"
        self.failures = 0
        self.failure_threshold = failure_threshold
        self.reset_timeout = reset_timeout
        self.opened_at = None

    def call(self, fn, *args):
        if self.state == "OPEN":
            if time.time() - self.opened_at > self.reset_timeout:
                self.state = "HALF_OPEN"
            else:
                raise CircuitOpenError()
        try:
            result = fn(*args)
            self._on_success()
            return result
        except Exception:
            self._on_failure()
            raise

    def _on_success(self):
        self.failures = 0
        self.state = "CLOSED"

    def _on_failure(self):
        self.failures += 1
        if self.state == "HALF_OPEN" or self.failures >= self.failure_threshold:
            self.state = "OPEN"
            self.opened_at = time.time()
Circuit breaker vs. retry — they compose, in a specific order
Retry answers "is this failure transient?" Circuit breaker answers "should I even try?" Wrap retry inside the breaker check, not the other way around — otherwise a breaker that's OPEN still gets hit by every retry attempt of every caller, defeating its entire purpose.

Retry Pattern

Solves: transient failures — a blip, a dropped packet, a momentary overload — that would succeed if simply attempted again a moment later.

The pattern only makes sense for errors that are actually transient and for operations that are safe to repeat. Retrying a 500 from an idempotent GET is safe. Retrying a non-idempotent "charge this card" POST without a dedupe key can double-charge a customer — see the Idempotency pattern later in this guide before wiring retry onto anything that mutates state.

Error typeRetry?Why
Network timeout, connection resetYesGenuinely transient
HTTP 503 / 429YesServer signaling temporary overload
HTTP 500 on a non-idempotent writeCarefulOnly with an idempotency key
HTTP 400 / 401 / 403NoThe request itself is wrong — retrying won't fix it
HTTP 404NoResource doesn't exist; retrying wastes a call

Timeout Pattern

Solves: hanging requests — a call with no timeout will wait forever for a dependency that will never respond, holding a thread and its resources hostage the entire time.

Every network call needs an explicit timeout — there is no safe default of "wait indefinitely." The harder part is picking the number: too short and you abort calls that would have succeeded; too long and a struggling dependency ties up your resources anyway, just more slowly.

Budget timeouts across a call chain
If service A calls B calls C, and each independently sets a 10-second timeout, a hang in C can cost A up to 30 seconds. Pass a shrinking deadline down the chain instead — A gives B a budget, B gives C whatever's left of that budget — so the whole chain fails together rather than compounding.

Bulkhead Pattern

Solves: failure isolation — one overwhelmed dependency exhausting the shared thread pool or connection pool that every other, unrelated dependency also relies on.

Named after ship design: watertight compartments mean one flooded section doesn't sink the whole vessel. In software, that means giving each downstream dependency its own dedicated pool of threads or connections, sized to that dependency's own risk and importance — so a slow, low-priority dependency can never starve the pool that a critical one depends on.

Fig. 3

Shared pool failure vs bulkheaded pools

Shared pool failure vs bulkheaded poolsLeft: one shared pool of 20 threads is entirely stuck waiting on slow Service C, so calls to Services A and B queue behind it. Right: per-dependency pools of 8, 8 and 4 threads; C's pool is saturated and failing while A and B are unaffected.queuedqueuedstarvedstarvedslowokokfailingA callsB callsC callsService AService BService CA callsB callsC callsService AService BService Cone slow dependency starves everyoneShared pool · 20 threadsall stuck waiting on slow Service Cisolated per-dependency poolsA pool8 threadsB pool8 threadsC pool4 threadsbusy threadidle thread (unused capacity)Shared pool→ Service A and B calls now queue behind C's mess tooBulkheaded→ C is fully saturated and failing; A and B are unaffected
Bulkheading trades some idle capacity (unused threads in A/B's pool while C's is slammed) for a hard guarantee that failures stay contained.

Fail Fast

Solves: wasted resources spent on a request that's already known to be doomed — checking cheap, definitive failure conditions immediately rather than partway through expensive processing.

Validate inputs, check circuit breaker state, and check resource availability at the very top of a request handler, before acquiring any locks, opening any connections, or doing any real work. A request that's going to be rejected should be rejected in microseconds, not after it's already consumed a database connection.

Fallback Pattern

Solves: graceful degradation — when a dependency is unavailable, returning something imperfect but useful is almost always better for the user than returning nothing.

Common fallback strategies

  • Serve stale cached data instead of a live, failed lookup
  • Return a sensible default (empty list, zero, "recommendations unavailable")
  • Degrade to a simpler algorithm (skip personalization, show generic results)
  • Queue the write for later instead of failing it outright

Where it pairs naturally

  • Circuit breaker OPEN → fallback response instead of an error
  • Cache miss + origin down → stale-if-error from the cache
  • Recommendation service down → non-personalized "popular items"

Hedged Requests

Solves: tail latency — the small fraction of requests that hit an unusually slow replica or path, which dominate p99 latency even when the median is fine.

Fire the same request to a second replica if the first hasn't responded within some threshold (often the p95 latency for that call), and take whichever answer comes back first, canceling the other. This trades a small amount of duplicate load for a large cut in tail latency — Google's "The Tail at Scale" paper found even a small hedge rate meaningfully improved p99s.

Fig. 4

Hedged request timeline

Hedged request timelineA request goes to Replica A at t=0; A is slow this time and takes 420ms. At t=95ms, the p95 threshold, a hedge is fired to Replica B, which answers in 90ms. The first response wins, so the client never waits for A's 420ms.Replica Arequest at t=0Replica Bhedge at t=95msReplica A — slow this time, 420ms1Replica B — 90ms2client never waits for this0ms95ms185ms420mstimehedge fires at t=95ms (p95 threshold)first response wins — client never waits for A's 420ms3
The hedge only fires after a threshold delay, not immediately alongside the first request — hedging every request from t=0 would double your total load for no benefit on the fast path.

Dead Letter Queue (DLQ)

Solves: failed message handling — a message that a consumer can't process (bad data, a downstream bug, an exhausted retry budget) either blocks the entire queue behind it, or gets silently dropped and lost forever. Neither is acceptable.

After N failed processing attempts, move the message to a separate dead letter queue instead of retrying forever or discarding it. This unblocks the main queue immediately and preserves the message for inspection, alerting, and manual or automated reprocessing later.

A DLQ is a signal, not a graveyard
Treat every message landing in a DLQ as an alert-worthy event, not a place to quietly forget about it. Un-monitored DLQs are one of the most common silent failure modes in production message systems — the queue does its job perfectly and nobody notices the pile of unprocessed orders for three days.
Interview drill: circuit breaker vs. retry vs. timeout — how do the three combine on a single call?

Timeout bounds how long you'll wait for any single attempt. Retry (with exponential backoff and jitter) decides whether to try again after a timeout or transient failure, and how many times. Circuit breaker sits above both, tracking the aggregate failure rate across many calls — once it trips OPEN, it skips the timeout-and-retry dance entirely and fails immediately, because retrying a dependency that's already known to be down just wastes time and adds more load to something already struggling.

Category 2 — Load Management Patterns

§2Load management: surviving too many requests, not too few

Resilience patterns assume a dependency is failing. Load management patterns assume the opposite problem: everything is healthy, there's just more demand than capacity, and something has to give — either you shape the traffic, or the traffic shapes your outage.

Rate Limiting

Solves: too many requests from a single client or in aggregate overwhelming a service, intentionally or not.

Rate limiting rejects requests once a client crosses a defined threshold, protecting the service from a single noisy client and giving fair access across many. The interesting engineering is entirely in which algorithm — see Token Bucket, Leaky Bucket, and Sliding Window below, each with real trade-offs.

Token Bucket

Solves: smoothing bursty traffic while still allowing legitimate short bursts, rather than enforcing a rigid constant rate that would reject perfectly reasonable spikes.

A bucket holds up to N tokens. Tokens refill at a fixed rate. Every request consumes one token; if the bucket is empty, the request is rejected or queued. Because tokens can accumulate while idle, a client that hasn't made a request in a while can burst up to the bucket's full capacity — this is the key difference from Leaky Bucket.

Fig. 5

Token bucket refill and consumption

Token bucket refill and consumptionA bucket with capacity 10 currently holds 4 tokens. It refills at +1 token per 100ms. Each arriving request consumes one token; if the bucket is empty the request is rejected with 429. An idle client's bucket fills back to 10.levelnoyesRefill+1 token / 100msRequest arrives1Bucketempty?2Allowedconsumes 1 tokenRejectedHTTP 429Bucket · capacity 101410maxburstsets the sustained rate4 / 10 tokens availableIdle client → bucket fills back to 10
The bucket's size sets the maximum burst; the refill rate sets the sustained long-run rate — the two are configured independently.
class TokenBucket:
    def __init__(self, capacity, refill_rate):
        self.capacity = capacity
        self.tokens = capacity
        self.refill_rate = refill_rate  # tokens per second
        self.last_refill = time.time()

    def allow(self):
        now = time.time()
        elapsed = now - self.last_refill
        self.tokens = min(self.capacity, self.tokens + elapsed * self.refill_rate)
        self.last_refill = now
        if self.tokens >= 1:
            self.tokens -= 1
            return True
        return False

Leaky Bucket

Solves: the same problem as token bucket, but where you need a strictly constant output rate — no bursts at all — because the downstream system genuinely cannot absorb them.

Requests fill a queue (the "bucket"); the queue drains at a fixed rate regardless of how fast it fills. Unlike token bucket, there's no accumulated allowance for bursting — the bucket smooths everything to one constant outflow rate, so it's the right choice when the consumer's constraint is truly about rate, not burst tolerance.

Token BucketLeaky Bucket
Allows burstsYes, up to bucket sizeNo — strictly smoothed
Output rateVariable, up to refill rate + burstPerfectly constant
Typical useAPI rate limitingTraffic shaping into a fixed-capacity pipe

Sliding Window (and Fixed Window)

Solves: accurate rate limiting at window boundaries — fixed windows let a client send double the intended rate by timing requests around the boundary.

The fixed-window boundary problem
With a 100 req/min fixed window, a client can send 100 requests at 0:59 and another 100 at 1:01 — 200 requests in a 2-second span, both technically within their respective windows. A sliding window (tracking a rolling log of timestamps, or a weighted blend of the current and previous window) closes this gap at the cost of more memory per client.

Queue-Based Load Leveling

Solves: sudden traffic spikes that would overwhelm a downstream service if applied directly — decoupling producers from consumers with a buffer in between.

Instead of calling a downstream service directly, requests are placed on a queue; the downstream service consumes from that queue at whatever pace it can sustain. The spike is absorbed by the queue's depth, and the consumer never sees more load than it can actually handle — at the cost of added latency for whoever is waiting on the result.

Backpressure

Solves: slow consumers being overwhelmed by fast producers — without backpressure, a fast producer either OOMs an unbounded queue or silently drops data.

The consumer signals upstream — "slow down" — and the producer respects that signal rather than pushing regardless. This can be as simple as a bounded queue that blocks the producer when full, or as explicit as TCP's own flow control window, or a reactive streams protocol like Flow.Subscription.request(n) where the consumer explicitly pulls only as much as it can process.

Load Shedding

Solves: a system already over capacity — rather than trying to serve everything slowly and risk failing all of it, load shedding deliberately drops low-priority requests to protect the ones that matter and keep the system itself alive.

Shedding vs. degrading silently under load
A system with no explicit shedding policy doesn't avoid this trade-off — it just makes it implicitly and badly: everything slows down together until the whole thing falls over, including the requests you most needed to succeed. Explicit shedding (reject low-priority traffic first, based on a defined priority signal) keeps the system serving something correctly rather than serving nothing at all.

Priority Queue (for traffic)

Solves: "VIP" traffic — some requests (a paying customer, a critical internal service, a health check) need to jump ahead of routine background traffic when a system is under load.

Tag requests with a priority at ingress and drain higher-priority queues first. This is what makes load shedding selective rather than random — shedding decisions become "drop from the lowest-priority queue first," not "drop whatever happens to be oldest."

Interview drill: why not just always use token bucket everywhere?

Token bucket is the right default for most API rate limiting because it tolerates reasonable bursts, which matches how real clients actually behave. But if a downstream system genuinely can't absorb any burst — a fixed-capacity hardware pipe, a strict SLA with a third party — leaky bucket's constant output rate is the correct tool, even though it will introduce latency that token bucket would have avoided for a bursty but otherwise-compliant client.

Category 3 — Caching Patterns

§3Caching: trading correctness-freshness for speed

Every caching pattern is the same trade dressed differently: some data is expensive to compute or fetch, so you keep a copy somewhere faster — and now you own the question of when that copy stops matching the truth.

Cache Aside (Lazy Loading)

Solves: read-heavy systems where most data is never touched — cache aside only populates the cache with data that's actually been requested, rather than pre-loading everything.

Fig. 6

Cache aside read path

Cache aside read pathThe app reads the cache first (1). On a MISS (2) it reads the database (3) and writes the result back to the cache (4). The application owns this logic; the cache never talks to the database.Owns the cache logicread cacheMISSread DBresultwrite result back to cache✕ no linkAppchecks cache, then DB,then fills cacheCacheknows nothing about the DBDatabase1234
The application owns the cache logic explicitly — the cache itself has no idea the database exists.
def get_user(user_id):
    cached = cache.get(f"user:{user_id}")
    if cached:
        return cached
    user = db.query("SELECT * FROM users WHERE id = ?", user_id)
    cache.set(f"user:{user_id}", user, ttl=300)
    return user

Read Through / Write Through

Solves: the same miss-then-fetch problem as cache aside, but pushes that logic into the caching layer itself rather than the application — the app only ever talks to the cache, and the cache transparently handles the database on a miss (read through) or writes to both synchronously (write through).

PatternWho owns the miss/write logicConsistency
Cache AsideApplication codeCache can go stale if writes bypass it
Read ThroughCache library/providerSame risk, less app code
Write ThroughCache library/providerCache and DB always in sync — but every write pays DB latency

Write Behind (Write Back)

Solves: write latency — write through pays the full database write cost on every request; write behind acknowledges the write once it's in the cache and flushes to the database asynchronously afterward.

The trade you're actually making
Faster writes, at the cost of a real durability gap: if the cache node dies before the async flush completes, that write is gone. Only use write behind for data where losing the last few seconds of writes on a rare crash is an acceptable risk — never for anything like a financial ledger.

Refresh Ahead is the read-side complement: proactively refresh a cache entry before it expires (typically when it's accessed and its TTL is getting close to zero), so a legitimately popular key never actually experiences a cache miss.

Cache Stampede Prevention

Solves: a cache miss storm — a popular key expires, and hundreds of concurrent requests all miss simultaneously and hammer the database at once with the identical query, sometimes hard enough to take the database down.

Lock / single-flight

Only the first request to miss actually queries the database; it acquires a short lock, and every other concurrent request for that same key waits on the result instead of also querying.

Probabilistic early expiration

Refresh a key slightly before its real TTL, with randomized jitter per key, so many keys with the same nominal TTL don't all expire in the same instant.

Bloom Filter

Solves: cache penetration — repeated lookups for keys that don't exist at all still miss the cache and hit the database every single time, since there's nothing to cache for a key with no data.

A Bloom filter is a compact probabilistic structure that answers "might this key exist?" with no false negatives but a tunable false-positive rate. Check it before querying the database: a definite "no" skips the database entirely; a "maybe" proceeds as normal. It costs a small amount of memory to eliminate a large class of pointless database hits from bots, scrapers, or bad IDs.

Fig. 7

Bloom filter guard in front of the database

Bloom filter guard in front of the databaseA lookup for key K first checks the Bloom filter. If K is definitely absent, the request is rejected and the database is skipped. If K is maybe present, it proceeds to the cache and database as normal.definitely absentmaybe presentLookup key KBloom filtermight this key exist?010011010010Reject — skip DBno wasted lookupProceed to cache / DBlookup runs as normalno false negatives — a real key is never blockedtunable false-positive rate
The filter only ever prevents wasted lookups for keys that are truly absent — it never blocks a real key, by construction.

Hot Key Replication

Solves: hot partitions — a single extremely popular key (a viral post, a celebrity's profile) can overload the one cache node or shard responsible for it, even while every other node sits idle.

Replicate the hot key across multiple cache nodes and route reads to a randomly chosen replica, spreading load for that one key across several machines instead of concentrating it on one. Detecting which keys are "hot" in the first place usually requires sampling access frequency in real time.

Interview drill: cache aside vs write through — which would you pick for a shopping cart service?

Write through, generally — cart contents need to be immediately consistent between cache and database, since a stale read right after a write (say, right after "add to cart") is a visible, confusing bug for the user. The extra write latency is an acceptable cost for a low-volume, correctness-sensitive write path. Cache aside would be a better fit for something read-heavy and more tolerant of brief staleness, like a product catalog page.

Category 4 — Messaging Patterns

§4Messaging: decoupling producers from consumers in time

Synchronous request/response ties a caller's fate to the callee's availability and speed. Messaging patterns break that coupling — the producer emits and moves on, the consumer processes on its own schedule, and the hard problems that surface are ordering, duplication, and partial failure across that gap.

Publish/Subscribe

Solves: event broadcasting — one event often needs to trigger action in several unrelated downstream systems (billing, analytics, notifications), and the publisher shouldn't need to know who's listening.

A publisher emits to a topic; any number of subscribers receive a copy independently. Adding a new subscriber never requires changing the publisher — this decoupling is the entire point, and it's what lets a system's downstream consumers grow over time without touching upstream code.

Event Sourcing

Solves: audit history — storing only the current state of an entity throws away exactly how it got there, which is often exactly what you need for debugging, compliance, or rebuilding derived views.

Instead of storing current state, store the full sequence of events that produced it (AccountOpened, Deposited $50, Withdrew $20) and derive current state by replaying them. The event log becomes the single source of truth; any view of "current state" is just a projection that can be rebuilt from scratch if it's ever wrong.

Fig. 8

Event log to current state

Event log to current stateAn append-only log of immutable events: AccountOpened, Deposited $50, Withdrew $20, Deposited $10. Replaying them produces the current balance of $40. Any bug in a derived view can be fixed by fixing the projection logic and replaying, not by data repair.Event log · append-only, immutablereplayrebuildAccountOpenedopened1Deposited $50+$502Withdrew $20−$203Deposited $10+$104balance$40Replaying the log rebuilds current stateand any bug in a derived view can be fixed by fixing the projection logic and replaying, not by data repairone possible projection
The events are immutable and append-only; "current balance" is just one possible projection of them.

CQRS (Command Query Responsibility Segregation)

Solves: read and write workloads with very different shapes — a single model optimized for one is usually a poor fit for the other, especially at scale.

Split the write model (commands, normalized, optimized for correctness and validation) from the read model (queries, often denormalized, optimized for the exact shape a UI needs). The two can even live in entirely separate datastores, kept in sync via the events emitted by the write side — which is why CQRS and Event Sourcing so often show up paired together.

CQRS adds real complexity — it's not a default
Two models means two things to keep consistent, and the read model is often eventually consistent with the write model rather than immediately so. Reach for CQRS when read and write really do have conflicting scaling or shape requirements — not as a default architecture for every service.

Saga Pattern

Solves: distributed transactions — a single business operation (place an order) that spans multiple services (payment, inventory, shipping) has no single database transaction that can wrap all of it atomically.

A saga breaks the operation into a sequence of local transactions, one per service, each with a defined compensating transaction to undo it if a later step fails. If step 3 of 4 fails, the saga runs the compensations for steps 2 and 1 in reverse — refund the payment, release the reserved inventory — rather than leaving the system in a half-completed state.

Choreography

Each service reacts to events from the previous step and emits its own — no central coordinator. Simpler for a few steps, but the overall flow is implicit and gets hard to trace as steps grow.

Orchestration

A central orchestrator explicitly calls each step and triggers compensations on failure. The flow is explicit and easy to trace, at the cost of a new single component to build and operate.

Outbox Pattern

Solves: reliable event publishing — writing to your database and separately publishing an event to a message broker is two operations with no shared transaction; a crash between them either loses the event or publishes one for a database write that never actually committed.

Write the event to an "outbox" table in the same database transaction as the actual business data change — so both succeed or both roll back together, atomically. A separate process then reads the outbox table and publishes each event to the broker, marking it published once confirmed.

Fig. 9

Outbox pattern flow

Outbox pattern flowThe service writes the business row to the orders table and the event to the outbox table in a single DB transaction. A relay / poller reads unpublished outbox rows, publishes them to the message broker, retries until acknowledged, and marks them published.Single DB transactionpollmark publishedpublishackretryServiceone transaction1orders tablebusiness dataoutbox tableevent to publishRelay / pollerreads unpublished rows2Message brokerreceives each event3both commit or both roll back
The write to the outbox table is transactionally atomic with the business write; publishing to the broker happens afterward and can be retried independently.

The Inbox Pattern is the consumer-side complement: track which message IDs have already been processed in a dedicated table, so a redelivered message (which brokers will do — see Delivery Guarantees) is recognized and skipped rather than processed twice.

Idempotent Consumer

Solves: duplicate messages — most real message brokers guarantee at-least-once delivery, meaning a consumer must expect to see the same message more than once and handle it safely.

Design message handlers so processing the same message twice produces the same result as processing it once — track processed message IDs (an inbox table, a Redis set with a TTL) and skip anything already seen. See the Idempotency pattern in the Reliability section for the general principle this pattern applies specifically to consumers.

Competing Consumers

Solves: parallel processing — a single consumer processing a queue serially caps throughput at that one consumer's speed, even when the work is fully parallelizable.

Run multiple consumer instances against the same queue; the broker hands each message to exactly one of them, so throughput scales roughly linearly with the number of consumers. The catch: message ordering across the whole queue is no longer guaranteed once more than one consumer is pulling from it — if order matters for a given entity, route related messages to the same consumer (partitioning by key, as Kafka does) rather than trying to preserve global order.

Interview drill: why does the outbox pattern need a separate transaction, not just "publish after commit"?

Because "commit the DB write, then publish the event" is two separate operations with a crash window between them. If the process dies after the commit but before the publish succeeds, the event is lost forever with no record it was ever supposed to happen. Writing the event into the same transaction as the business data guarantees the two either both happen or neither does — the event's existence in the outbox table is itself durable and transactional, and publishing from there becomes a retryable, idempotent operation instead of a single point of loss.

Category 5 — Database Patterns

§5Database patterns: scaling storage without losing your data

A single database server can only hold so much data and answer so many queries per second. These patterns are how systems keep growing past that ceiling — splitting data up, copying it around, and controlling who gets to change it and when.

Sharding

Solves: horizontal scaling of storage and write throughput — a dataset too large or too write-heavy for one machine gets split across many, each holding a subset ("shard") of the total data.

The critical decision is the shard key — the field used to decide which shard a given row lives on. A well-chosen key spreads load evenly; a poor one (say, sharding by signup date when all recent traffic hits recent signups) creates a hot shard that defeats the entire point of sharding.

What sharding costs you
Cross-shard joins and transactions become genuinely hard — a query spanning two shards either needs an application-level join or a distributed transaction protocol, both significantly more complex than a single-node query. Shard only once a single node's capacity is the actual, measured constraint.

Read Replicas & Leader-Follower Replication

Solves: read scalability and high availability — most systems read far more than they write, so directing all reads at one primary wastes the ability to scale reads independently.

One leader accepts writes and replicates them (synchronously or asynchronously) to one or more followers, which serve read traffic. Asynchronous replication means followers can lag behind the leader by some amount — this is replication lag, and it's the direct cause of "I just wrote something and then couldn't see it on the very next read."

Fig. 10

Leader-follower replication with lag

Leader-follower replication with lagA client writes to the leader, which replicates asynchronously to Follower A with 12ms of lag and Follower B with 4.2s of lag. A read routed to Follower B will not see a write from 3 seconds ago, so a follow-up read goes to the leader.writefollow-up readClientwrites, then readsLeader (writes)accepts every writeLag depends onthe follower's own loadits network pathits replay speedFollower Alag: 12msFollower Blag: 4.2sasync replication↑ a read routed here won't see a write from 3 seconds ago
Lag isn't constant across followers — it depends on that follower's own load, network path, and replay speed, which is why "read your own writes" often routes the immediate follow-up read to the leader.

Leader-leader (multi-leader) replication accepts writes at more than one node, usually one per region for low-latency local writes — at the cost of needing an explicit conflict-resolution strategy (last-write-wins, CRDTs, or vector clocks — see below) for when two leaders accept conflicting writes to the same record.

Consistent Hashing

Solves: even distribution across nodes while minimizing data movement when a node is added or removed — naive hash(key) % N reshuffles almost every key's assigned node the instant N changes.

Nodes and keys are both hashed onto the same conceptual ring; a key belongs to the first node found walking clockwise from its position. Adding or removing one node only reassigns the keys between it and its ring neighbor — every other key stays exactly where it was.

Fig. 11

Consistent hashing ring

Consistent hashing ringNodes and keys are hashed onto one ring; each key belongs to the next node clockwise. Going clockwise the nodes are D, B, C, A. Removing Node B moves only the keys between D and B, to C; every other key stays put, unlike hash(key) % N.Walk clockwisea key belongs to the first nodefound walking clockwise from it1Remove Node Bonly the keys between D and B move —to C, its new clockwise neighbor2Everything else stays putevery other key stays exactlywhere it was3Naive hash(key) % Nreshuffles almost every key's nodethe instant N changesnodes + keyshashed onto one ringDBCAB's keys move to Cif Node B is removedkeykeys owned by the next node clockwiseNode DNode BNode CNode Akey → B (clockwise)vs.
Each dark dot is a key; it belongs to whichever node is next clockwise. Remove Node B and only the keys between D and B move — to C, its new clockwise neighbor.

Materialized Views

Solves: expensive queries run repeatedly — a complex join or aggregation recomputed on every request wastes work when the underlying data changes far less often than it's read.

Precompute and store the query's result as its own table, refreshed periodically or on a trigger from the underlying data changing. Reads become a simple lookup; the cost moves to keeping the view updated, which is a deliberate trade of freshness for read speed.

Optimistic vs. Pessimistic Locking

Solves: concurrent updates to the same row — two transactions modifying the same data at once need a strategy to prevent one from silently overwriting the other's work.

Optimistic LockingPessimistic Locking
MechanismVersion column checked on write; write fails if version changedSELECT ... FOR UPDATE — actually holds a lock
Blocks other readers/writers?No — they proceed freelyYes — they wait
Best forLow contention, most writes don't actually conflictHigh contention, conflicts are common and cheap to just wait for
Failure modeCaller must detect the conflict and retryRisk of deadlock between multiple lock holders
Interview drill: you're seeing "lost updates" on a row two services both write to — which locking strategy, and why?

Depends on how often the two services actually collide. If it's rare — most writes touch different rows or happen at different times — optimistic locking with a version column is lighter weight: no lock is held, most writes just succeed, and only the rare genuine conflict needs a retry. If the two services are frequently racing for the exact same row, pessimistic locking is more predictable, because optimistic locking under heavy contention just means most writes fail and retry repeatedly, which can be worse than briefly blocking.

Category 6 — Distributed System Patterns

§6Distributed systems: agreement without a shared clock or a single point of truth

Everything in this section exists because of one hard fact: in a distributed system, there is no shared, reliable notion of "now," and any message can be delayed, lost, or arrive out of order. You cannot tell the difference between "that node is slow" and "that node is dead" from the outside — every pattern here is a strategy for making progress anyway, despite that fundamental uncertainty.

Leader Election

Solves: needing exactly one coordinator among a group of equivalent nodes — for accepting writes, scheduling work, or breaking ties — without a human manually assigning the role and without two nodes both believing they're in charge at once.

Nodes run a protocol (Bully algorithm, Raft's leader election, or a lease held in a coordination service like ZooKeeper or etcd) to agree on a single leader. The leader typically holds a time-bound lease it must periodically renew; if it stops renewing (because it crashed, or is partitioned away), the lease expires and the remaining nodes elect a new one.

Fig. 12

Leader election via lease renewal

Leader election via lease renewalNode B is leader and renews its lease every 2s while A and C are followers. B crashes and the lease is not renewed. The lease expires, A and C race to acquire it, and C wins and becomes the new leader.1 · Steady state2 · Leader crashes3 · Lease expiresrenew every 2s✕ no renewalracewinsLeaseheld by Node BNode AfollowerNode BLEADERNode CfollowerLeasenot renewedNode AfollowerNode BcrashedNode CfollowerLease expiredA and C race to acquireNode AfollowerNode BdownNode Cnew leaderB renews the lease every 2sNode B crashes — lease not renewedC wins, becomes new leader
The lease's timeout is the whole safety mechanism — too short and healthy leaders get evicted by transient network blips; too long and the system is leaderless for that whole duration after a real crash.

Service Discovery

Solves: dynamic instances — in an environment where service instances are created and destroyed constantly (autoscaling, deploys, crashes), hardcoded IP addresses break immediately.

Client-side discovery

The client queries a registry (Consul, etcd, ZooKeeper) directly and picks an instance itself. Fewer moving parts, but every client needs registry-aware logic.

Server-side discovery

The client calls a fixed address (a load balancer or service mesh sidecar); that layer queries the registry on the client's behalf. Simpler clients, one more infrastructure component.

Either way, instances register themselves on startup and deregister (or get removed via failed health checks) on shutdown — the registry's accuracy depends entirely on this registration/deregistration discipline actually being reliable.

Heartbeat

Solves: failure detection — in a distributed system, you can't directly observe whether a remote node is alive; a heartbeat is the mechanism that lets you infer it.

Each node periodically sends a lightweight "I'm alive" signal. Miss enough consecutive heartbeats and the node is presumed dead — but this is a probabilistic judgment, not a certainty: a node can be alive but partitioned from the network, indistinguishable from actually being dead to everyone else. This ambiguity is precisely why distributed systems need consensus protocols rather than simple heartbeat-based assumptions for anything safety-critical.

Gossip Protocol

Solves: cluster-wide communication without every node needing to talk to every other node directly — a naive all-to-all broadcast doesn't scale past a small cluster size.

Each node periodically shares what it knows with a small, random subset of other nodes; information spreads exponentially through the cluster the way a rumor spreads through a social network, reaching every node in O(log N) rounds without any single node needing global connectivity. Cassandra and Consul both use gossip for cluster membership and failure detection.

Fig. 13

Gossip spread across three rounds

Gossip spread across three roundsRound 0: one node knows. Round 1: it tells 2 random peers, so 3 know. Round 2: each informed node tells 2 random peers and nearly all nodes are infected. The informed count roughly doubles each round.Round 0Round 1Round 2one node learns somethingtells 2 random peersnearly all infectedinformednot yetnewly told this round1 / 8 informed3 / 8 informed7 / 8 informedinformed count ≈ doubles each round → O(log N) rounds
Exponential spread means the number of "informed" nodes roughly doubles each round — a cluster of thousands converges in a handful of rounds.

Distributed Lock

Solves: shared resource access across multiple processes on different machines — a normal in-process mutex only protects against concurrent access within one process; a distributed lock is needed when the contenders are separate machines entirely.

Typically implemented on top of a coordination service (ZooKeeper, etcd, or Redis via the Redlock algorithm) as a lease with a TTL: a client acquires the lock, does its work, and releases it, or the lease expires automatically if the client crashes mid-operation.

The classic distributed-lock bug: the pause-after-acquire
A client acquires the lock, then experiences a long GC pause or network stall well past the lock's TTL. The lock silently expires and another client acquires it, believing it now has exclusive access — while the first client, unaware it lost the lock, resumes and keeps acting as if it's still the exclusive holder. Both are now operating on the resource simultaneously. Defending against this requires a monotonically increasing "fencing token" issued with the lock, checked and enforced by the resource being protected itself — not by the lock service, which has no way to stop the first client from acting on stale information.

Two-Phase Commit (2PC) & Three-Phase Commit (3PC)

Solves: strong, atomic consistency across multiple independent databases or services participating in one logical transaction.

Two-Phase Commit

Phase 1 (prepare): coordinator asks every participant "can you commit?" and each locks its resources and replies yes/no. Phase 2 (commit): if all said yes, coordinator tells everyone to commit; if any said no, tells everyone to abort.

The blocking problem

If the coordinator crashes after phase 1 but before phase 2, every participant is stuck holding its locks indefinitely, unable to know whether to commit or abort — this is 2PC's core weakness.

Three-Phase Commit adds an intermediate "pre-commit" acknowledgment step specifically to reduce (not eliminate) this blocking window, at the cost of one more network round trip on every transaction. In practice, most modern systems avoid both altogether in favor of the Saga pattern's eventual consistency, precisely because blocking on a coordinator is such an availability risk.

Quorum Reads/Writes

Solves: tunable consistency in a replicated system — rather than requiring every replica to acknowledge a write (which kills availability the moment any one replica is slow) or just one (which risks reading stale data), quorum lets you require a majority.

With N replicas, a write requires acknowledgment from W of them, and a read requires responses from R of them. The guarantee W + R > N ensures every read overlaps with the most recent write in at least one replica — giving you strong consistency without needing all N nodes to participate in every operation.

ConfigurationProperty
W = N, R = 1Fast reads, slow/fragile writes (any replica down blocks writes)
W = 1, R = NFast writes, slow/fragile reads
W = R = (N/2)+1Balanced — the common choice; tolerates minority node failures for both

Consensus (Raft / Paxos)

Solves: getting a cluster of unreliable nodes, communicating over an unreliable network, to agree on a single value or sequence of operations — the foundational problem underneath leader election, distributed locks, and replicated logs alike.

Raft (designed explicitly to be more understandable than Paxos) works in rounds called terms: a leader is elected per term and replicates a log of operations to followers, committing an entry only once a majority of nodes have durably stored it. Paxos solves the identical underlying problem with a more general but notoriously harder-to-implement-correctly two-phase proposal/accept protocol. Both guarantee safety (no two nodes disagree on a committed value) as long as a majority of nodes are healthy and can communicate.

Why this is the pattern everything else quietly depends on
etcd runs Raft. ZooKeeper runs a Paxos-like protocol called ZAB. Every "distributed lock" and "leader election" pattern above that uses one of these systems is, underneath, relying on consensus to make sure the cluster agrees on who holds the lock or who the leader is — consensus isn't a peer to those patterns, it's the substrate they're built on.

Vector Clocks

Solves: conflict resolution when there's no global clock to say definitively which of two concurrent writes "happened first" — wall-clock timestamps from different machines can't be trusted to agree, and even if they could, two writes can be genuinely concurrent with no real ordering between them.

Each node keeps a vector of counters, one per node, incremented on every local update and merged (taking the max per position) on every message received. Comparing two vector clocks tells you whether one write causally preceded the other, or whether they were truly concurrent and need explicit conflict resolution (last-write-wins, application-level merge, or presenting both to the user, as early Amazon Dynamo famously did with shopping carts).

Interview drill: what's the actual difference between a heartbeat failure and a network partition, and why does it matter?

From the observer's side, they're indistinguishable — both look like "stopped responding." The difference matters because the correct response is different: a genuinely dead node can safely be replaced, its locks released, its role reassigned. A partitioned-but-alive node might still be doing work, might still hold a lock it believes is valid, and might rejoin later with stale state. This is exactly why lock services use fencing tokens and why consensus protocols require majority agreement rather than trusting any single node's judgment about who else is alive — the system has to be correct even when it can never be fully sure which case it's in.

Interview drill: quorum reads/writes vs. 2PC — both involve "getting enough nodes to agree." What's actually different?

2PC coordinates one atomic transaction across distinct participants that each need to do something different (debit here, credit there) and all must agree before any of them commits — it's about atomicity across heterogeneous operations. Quorum is about replication of the same data across homogeneous copies, and it deliberately allows partial participation (W or R out of N) rather than requiring unanimous agreement, trading strict atomicity for availability. 2PC can block indefinitely on a coordinator failure; quorum systems are specifically designed to keep functioning as long as a majority of replicas are reachable.

Category 7 — Traffic Routing Patterns

§7Traffic routing: getting requests to the right place, safely

Once you have more than one instance of a service — or more than one version of it — something has to decide which instance handles which request, and how a new version gets introduced without breaking everything currently in flight.

Load Balancer

Solves: traffic distribution — spreading incoming requests across multiple backend instances so no single instance is overwhelmed while others sit idle.

AlgorithmBehavior
Round robinCycles through instances in order — simple, ignores actual instance load
Least connectionsSends to whichever instance has the fewest active connections
WeightedSome instances get proportionally more traffic (useful for mixed instance sizes)
Consistent hashSame client/key routes to the same instance — needed for sticky sessions

Sticky Sessions pin a given client to the same backend instance for the life of their session — necessary when session state lives in that instance's memory rather than a shared store, at the cost of uneven load if some clients are far more active than others.

API Gateway

Solves: centralized routing and cross-cutting concerns — without a gateway, every client needs to know the address of every backend service, and concerns like auth, rate limiting, and logging get duplicated into each service individually.

A single entry point that routes requests to the correct backend service and handles authentication, rate limiting, request/response transformation, and logging in one place. Related: a Reverse Proxy does the routing/forwarding piece without necessarily owning the cross-cutting policy layer — an API gateway is a reverse proxy with opinions.

A BFF (Backend for Frontend) takes this further with a dedicated gateway per client type (web, iOS, Android), each shaping responses exactly to that client's needs rather than forcing one generic API to serve all of them equally awkwardly.

Service Mesh

Solves: service-to-service communication concerns (retries, timeouts, mTLS, observability, traffic splitting) at scale, without embedding that logic in every service's own codebase in every language they happen to be written in.

A sidecar proxy (Envoy is the most common) runs alongside every service instance and intercepts all its network traffic, applying policy uniformly regardless of what language the service itself is written in. Istio and Linkerd are control planes that configure the sidecars fleet-wide.

Fig. 14

Service mesh sidecar pattern

Service mesh sidecar patternPod A runs Service A beside a sidecar proxy; Pod B runs a sidecar beside Service B. All traffic passes through the sidecars, which a control plane such as Istio or Linkerd configures. Service code has zero knowledge of retries, mTLS or tracing — the sidecar owns all of it.Pod APod BOwned by the sidecar, not the serviceall traffic passesthrough sidecarsconfiguresconfiguresService Abusiness codesidecarproxysidecarproxyService Bbusiness codeControl planeIstio / LinkerdService A/B code has zero knowledge of retries, mTLS, or tracingretriestimeoutsmTLStracing
The Sidecar pattern (see Advanced Patterns below) is the general mechanism; a service mesh is that mechanism applied specifically to networking concerns.

Blue-Green Deployment

Solves: zero-downtime releases — deploying a new version by replacing the old one in place risks a window where neither version is fully healthy.

Run two identical environments, "blue" (current production) and "green" (new version). Deploy and fully test the new version on green while blue keeps serving all live traffic, then switch the router to green atomically. If anything's wrong, switch back to blue instantly — the previous version is still running, not torn down.

Canary Deployment

Solves: safe rollout — blue-green switches 100% of traffic at once; canary reduces the blast radius of a bad release by exposing it to a small slice of real traffic first.

Route a small percentage of traffic (often 1–5%) to the new version, watch error rates and latency closely, and ramp up gradually if metrics stay healthy — or roll back instantly, affecting only that small slice, if they don't. Shadow Traffic goes a step further for the most cautious rollouts: mirror real production traffic to the new version without it affecting any real user, comparing its outputs against the current version silently.

Feature Flags

Solves: controlled releases decoupled from deployment — without flags, "ship the code" and "turn on the feature" are the same event, which means the only way to disable a bad feature is another deploy.

Wrap new functionality in a runtime-checked flag so it can be toggled, gradually rolled out to a percentage of users, or targeted at specific segments — all without a new deploy. This also enables A/B testing and instant kill-switches for anything that turns out to be broken in production.

Interview drill: blue-green vs. canary — when would you choose one over the other?

Blue-green is faster to roll back completely (instant full switch, either direction) and simpler to reason about, but it's all-or-nothing — a subtle bug that only shows up under 1% of real traffic patterns won't surface until 100% of users hit it. Canary is slower to fully roll out but catches exactly that kind of subtle, traffic-dependent issue early, with a much smaller blast radius while you find out. Many real pipelines use both: canary first to catch problems early, then a blue-green-style full cutover once the canary's metrics look clean.

Category 8 — Reliability Patterns

§8Reliability: doing exactly the right amount of work, exactly once (or knowing when you can't)

Networks drop acknowledgments even when the underlying operation succeeded. This section is about the guarantees you can actually make when you can't always tell whether something already happened.

Idempotency

Solves: safe retries — if retrying an operation is going to be a core part of how the system handles failure (and it always is), that operation needs to produce the same result no matter how many times it's actually executed.

The standard mechanism is an idempotency key: the client generates a unique key per logical operation and sends it with the request; the server checks whether it's already processed that key and, if so, returns the original result instead of executing the operation again.

def charge_card(idempotency_key, amount, card):
    existing = db.get_by_idempotency_key(idempotency_key)
    if existing:
        return existing.result   # already processed -- return the original outcome
    result = payment_processor.charge(amount, card)
    db.save_idempotency_record(idempotency_key, result)
    return result
Idempotent ≠ safe to call twice for free
Idempotency guarantees the same result, not that the second call does nothing. A well-implemented idempotent charge endpoint still does a database lookup on every retry — it just skips re-running the actual charge. This is why idempotency keys need their own storage with a sensible retention window, not an assumption that duplicate calls are magically free.

Delivery Guarantees: At-Least-Once, At-Most-Once, Exactly-Once

Solves: defining precisely what a messaging system promises about how many times a message will be delivered — a prerequisite for knowing what your consumer actually needs to defend against.

GuaranteeBehaviorRisk
At-most-onceSend, don't confirm, move onMessage can be silently lost
At-least-onceRetry until acknowledgedMessage can be delivered more than once
Exactly-onceDelivered and processed exactly one time, guaranteedGenuinely hard to achieve end-to-end; usually really "at-least-once delivery + idempotent processing" underneath
"Exactly-once" is almost always a composed guarantee
True exactly-once delivery across an unreliable network is provably very difficult (it runs into the same two-generals problem underlying 2PC's blocking issue). What most systems that advertise "exactly-once processing" actually deliver is at-least-once delivery paired with an idempotent consumer — the message might arrive twice, but processing it twice produces the same effect as processing it once, so the end result looks exactly-once even though the delivery mechanism isn't.

Checkpointing & Snapshotting

Solves: recovery after a crash — without periodic checkpoints, a long-running stream processing job or stateful service has to replay its entire history from the beginning after any failure.

Periodically persist enough state to resume from that point rather than from the start — a stream processor saves its position in the input log plus any accumulated aggregation state; a database takes a full snapshot periodically so its write-ahead log doesn't have to be replayed from the beginning of time. Snapshotting is checkpointing applied to full system state rather than a single job's progress.

Health Checks & Watchdog

Solves: detecting failures automatically rather than relying on a human noticing — the prerequisite for automated recovery, load balancer routing decisions, and orchestrator restart decisions.

Liveness vs. readiness

Liveness: "is this process running and not deadlocked?" A failed liveness check triggers a restart. Readiness: "can this instance currently serve traffic?" A failed readiness check just removes it from the load balancer's rotation — it might come back readily once, say, its cache warms up.

Watchdog

A separate monitoring process that watches for hung or unresponsive processes and forcibly restarts them — a safety net for failure modes (deadlocks, infinite loops) that a process can't detect or fix in itself.

Interview drill: your payment API needs to guarantee a customer is never double-charged, even if the network drops the response after a successful charge. What do you build?

The client generates an idempotency key per checkout attempt and sends it with the charge request. If the network drops the response, the client's retry logic (exponential backoff, per the Resilience section) retries with the same key. The server looks up that key before doing anything: if it's already been processed, it returns the stored result immediately without touching the payment processor again; if not, it processes the charge and stores the result keyed by that idempotency key. The client never needs to know whether its first attempt actually succeeded before the network dropped — the retry is safe by construction.

Category 9 — Scalability Patterns

§9Scalability: adding capacity without adding coupling

Scaling is easy in theory — add more machines — and hard in practice, because most of the difficulty is in the assumptions your system quietly baked in about running on exactly one machine.

Horizontal vs. Vertical Scaling

Vertical (scale up)Horizontal (scale out)
MethodBigger machine — more CPU/RAMMore machines
CeilingHard limit — biggest machine money buysNo practical ceiling
App changes neededUsually noneRequires statelessness, load balancing, often data partitioning
Failure impactOne machine down = fully downOne instance down = reduced capacity, not outage

Auto Scaling is horizontal scaling made dynamic: instance count adjusts automatically based on real-time load (CPU, queue depth, request rate) rather than a fixed, manually-set count — capacity grows to meet demand and shrinks back down when it passes, rather than being permanently provisioned for peak.

Stateless Services

Solves: the actual prerequisite for horizontal scaling and load balancing to work at all — if a service holds session or request state in its own memory, requests from the same client must always land on the same instance, defeating the point of having many interchangeable instances.

Push state out of the application process entirely — into a shared cache, a database, or a token the client holds itself (a signed JWT, for instance) — so that any instance can handle any request. This is also precisely what makes Blue-Green and Canary deployments, and auto scaling in general, straightforward: instances become disposable and interchangeable.

Worker Pools & Thread/Connection Pools

Solves: controlled concurrency — creating a new thread or a new database connection per task is expensive (see the thread pool internals covered in Java concurrency guides); pools reuse a fixed, warm set instead.

A worker pool applies the same idea at the process or job level: a fixed number of worker processes pull tasks from a shared queue, giving parallel execution with a hard, predictable ceiling on total resource usage — the same shape as a thread pool, just at a coarser grain. Microservices apply the same underlying idea at the architecture level: splitting a system so each piece can be scaled independently, based on its own specific load, rather than scaling one large monolith uniformly regardless of which part is actually the bottleneck.

Interview drill: why is statelessness described as a prerequisite for scaling, not a scaling pattern itself?

Because none of horizontal scaling, load balancing (beyond trivial round robin with no session affinity), auto scaling, or blue-green/canary deployment actually work correctly if an instance holds state that only it has. A stateful instance can't be freely replaced, added, or removed without losing or stranding that state — which is exactly what all of those other patterns need to be able to do at will. Statelessness isn't one tool among many; it's the property that makes the rest of the toolbox usable.

Category 10 — Security Patterns

§10Security: who is this, and what are they allowed to do

Every distributed system eventually needs to answer two separate questions for every request: who is making it (authentication), and what are they allowed to do (authorization) — plus, once you have more than one service, how services trust each other.

OAuth2, JWT, RBAC, ABAC, API Keys

MechanismSolves
OAuth2Delegated authorization — letting an app act on a user's behalf without ever seeing their password
JWTStateless auth — a signed token carrying identity/claims, verifiable without a database lookup on every request
RBACRole-based permissions — "admins can delete, viewers can only read"
ABACAttribute-based permissions — finer-grained rules like "can edit if owner AND document is unlocked"
API KeysSimple service-to-service or third-party auth, no user context needed
A JWT's statelessness is also its risk
Because a JWT is self-contained and verified by signature alone, revoking one before its expiry is genuinely awkward — there's no central "is this token still valid?" check by design. Short expiry times plus a refresh-token flow are the usual mitigation, trading a little complexity back in exchange for bounding how long a compromised token stays useful.

mTLS & Secrets Management

mTLS (mutual TLS) solves service authentication in the reverse direction from normal TLS: not just the client verifying the server's certificate, but the server also verifying the client's — the foundation of "zero trust" service-to-service communication inside a service mesh, where no internal call is trusted just because it originated inside the network perimeter.

Secrets Management solves credential storage: database passwords, API keys, and certificates need to exist somewhere, and "hardcoded in a config file checked into git" is the single most common real-world security failure. A dedicated secrets manager (Vault, AWS Secrets Manager) stores them encrypted, controls access via policy, and can rotate them automatically.

Category 11 — Observability Patterns

§11Observability: understanding a system you cannot fully see

In a distributed system, no single machine has the whole picture of what happened for any given request. Observability patterns are how you reconstruct that picture after the fact.

Distributed Tracing & Correlation IDs

Solves: request tracking across service boundaries — a single user request in a microservices architecture might touch a dozen services, and understanding where time was actually spent (or where it failed) requires following that one request's path across all of them.

A correlation ID is generated at the edge and passed through every downstream call, tagging every log line and span with the same identifier so they can all be reassembled later. Distributed tracing (Jaeger, Zipkin) builds on this: each service creates a "span" recording how long its part took, and spans are linked into a full trace showing the request's entire path and timing, service by service.

Fig. 15

A distributed trace as a waterfall of spans

A distributed trace as a waterfall of spansTrace 8f3a… reassembles one request: API Gateway 210ms total, containing Order Service 160ms, which contains Inventory check 70ms followed by Payment 90ms — the slow span.API Gatewaytrace_id 8f3a...210ms totalOrder Servicetrace_id 8f3a...160msInventory checktrace_id 8f3a...70msPaymenttrace_id 8f3a...90ms (slow!)(one request, reassembled from every service it touched)0ms50ms100ms150ms200mstrace_id: 8f3a...
Without a shared trace ID, this exact breakdown — which downstream call actually cost the 90ms — would require manually correlating timestamps across three separate services' logs.

Structured Logging & Metrics

Structured Logging solves searchable logs: plain-text log lines are only searchable by string matching; logging as structured key-value data (JSON) makes logs queryable by field — "show me every request with status=500 and service=payment" — rather than grepping for substrings.

Metrics solve system health at a glance: counters, gauges, and histograms (request rate, error rate, p50/p95/p99 latency) aggregated over time, cheap enough to collect at high volume and query in real time, unlike full log lines. OpenTelemetry has become the de facto standard for emitting all three — traces, logs, and metrics — through one unified, vendor-neutral instrumentation layer.

Interview drill: a request is slow. Would you reach for metrics or a distributed trace first, and why?

Metrics first — they tell you that something is slow and roughly which service or endpoint, cheaply, across all traffic, and they're what your alerting is watching anyway. A distributed trace is the tool for why a specific request was slow: once metrics point at "payment service p99 spiked," pulling a trace for one of the affected requests shows exactly which downstream call inside payment actually ate the time. Metrics find the haystack; tracing finds the needle once you're already looking in the right one.

Category 12 — Advanced & Structural Patterns

§12Advanced patterns: the vocabulary for everything else

These don't fit neatly into one failure category — they're structural patterns for organizing code and traffic that show up constantly once a system has any real size.

Request Coalescing / SingleFlight

If N identical concurrent requests arrive for the same uncached key, execute the underlying work exactly once and fan the single result out to all N callers — directly related to Cache Stampede Prevention, generalized beyond just caching.

Write Coalescing

Batch multiple small writes to the same target into one larger write, trading a little latency for dramatically fewer, more efficient I/O operations.

Fan-Out/Fan-In & Scatter-Gather

Dispatch one request to many downstream services in parallel (fan-out/scatter), then merge their independent results into one response (fan-in/gather) — the shape behind most search and aggregation endpoints.

Strangler Fig Pattern

Incrementally replace a legacy system by routing an ever-growing slice of traffic to new services while the old monolith keeps serving the rest — until eventually nothing depends on the original system and it can be retired.

Sidecar Pattern

Deploy a helper process alongside a main service (same pod, same lifecycle) to handle a cross-cutting concern like logging, proxying, or config — the general mechanism behind a service mesh's per-instance proxy.

Ambassador Pattern

A specific flavor of sidecar dedicated to representing outbound network calls on the main service's behalf — handling retries, circuit breaking, or protocol translation for calls the main service makes, without the main service's own code needing to know.

Anti-Corruption Layer

A translation layer between a new system's clean domain model and a legacy system's messier one, so the legacy system's quirks and inconsistencies don't leak into and corrupt the new system's design.

Choreography vs. Orchestration

Covered under Saga above — the general question of whether coordination emerges from services reacting to each other's events, or is directed by a central controller.

Event-Carried State Transfer

Events carry the full changed state, not just an "X happened, go look it up" notification — so a downstream consumer can update its own local copy without an extra synchronous call back to the source.

Command Pattern

Encapsulate an action as an object (with its own parameters and execute method) rather than a direct method call — the classic OOP building block underneath queued tasks, undo/redo, and job scheduling.

Repository & Unit of Work

Repository abstracts data access behind a domain-friendly interface, hiding the actual storage mechanism. Unit of Work groups several such changes into one atomic save, tracking everything modified in a business operation and committing it together.

Proxy & Adapter Pattern

Proxy stands in for another object to control or augment access to it (lazy loading, access control, caching). Adapter converts one interface into another a caller already expects — the standard tool for integrating a system that speaks a different "shape" than the one your code was written against.

Cheat sheet: symptom → pattern

SymptomReach for
Retries all firing at once, worsening an outageExponential Backoff + Jitter
One dead dependency cascading into total outageCircuit Breaker + Bulkhead
Requests hanging forever on a stuck dependencyTimeout (with a budget across the call chain)
A client sending far more requests than intendedRate Limiting — Token Bucket for bursty, Leaky Bucket for strict
System falling over entirely under peak loadLoad Shedding + Priority Queue
Cache expiring and every request hammering the DB at onceCache Stampede Prevention (single-flight / early refresh)
Repeated lookups for keys that don't existBloom Filter
One business operation spanning several services, no shared transactionSaga Pattern + Outbox Pattern
Consumer might see the same message twiceIdempotent Consumer + Idempotency keys
Database too large / too hot for one nodeSharding + Consistent Hashing
Reads far outnumber writesRead Replicas + Cache Aside
Need exactly one coordinator, no split-brainLeader Election (built on Consensus)
Multi-machine mutual exclusionDistributed Lock — with fencing tokens
Releasing new code without downtime or big-bang riskBlue-Green for instant cutover, Canary for gradual, cautious rollout
Can't tell why one specific request was slowDistributed Tracing + Correlation IDs

Resources

Foundational papers
  • Google — "The Tail at Scale" (hedged requests, tail latency at scale)
  • Amazon — the original Dynamo paper (vector clocks, quorum, eventual consistency)
  • Ongaro & Ousterhout — "In Search of an Understandable Consensus Algorithm" (Raft)
  • Lamport — the original Paxos papers
Books
  • Designing Data-Intensive Applications — Martin Kleppmann — the definitive modern text on almost everything in the Database and Distributed Systems sections above
  • Release It! — Michael Nygard — the original popularizer of Circuit Breaker and Bulkhead in production contexts
  • Enterprise Integration Patterns — Hohpe & Woolf — the source for most of the Messaging section's vocabulary
Practice
  • Read the source of a resilience library (resilience4j, Polly, Hystrix's retired-but-still-instructive codebase) against the Circuit Breaker and Bulkhead sections above
  • Trace one real request through a system you work on and identify which of these 50+ patterns are already quietly present in the path — most production systems use far more of this list than any single engineer realizes