Handbook · Digital · Software

Systems & Architecture

Systems & Architecture··12 min read

TL;DR

Every system lives somewhere on a ladder that starts at one thread in one process and ends at a product spread across regions. Each rung adds a new primitive — thread, socket, queue, consensus — and a new failure mode that the rung below didn't have. The mental model at each rung is different; the ladder is the same. Learn the rungs; you'll recognize every new problem as a return to a rung you've seen.

You will be able to

  • Name the six rungs and the primitive each one introduces.
  • For any system, identify which rung its current problem lives at.
  • Predict the failure mode a proposed design inherits from its highest rung.

The Map

Rendering diagram…

Every rung is a superset of the previous — you inherit all of the lower failure modes plus the new one. A region-replicated system can still have a deadlock on a single host.

Rung 1 — One process, one thread

The baseline: a program with a call stack, a heap, system calls. No concurrency. Failure modes are the ones we teach first: logic bugs, infinite loops, OOM, file-not-found.

     ┌─────────────────────────────────────┐
     │            PROCESS                  │
     │  ┌────────────┐   ┌──────────────┐  │
     │  │   stack    │   │     heap     │  │
     │  │ ────────── │   │              │  │
     │  │   main()   │   │   new Foo()  │  │
     │  │   f()      │   │   new Bar()  │  │
     │  │   g()      │   │              │  │
     │  └────────────┘   └──────────────┘  │
     │         │                           │
     └─────────┼───────────────────────────┘
               ▼ syscalls: read, write, mmap, …
             KERNEL

The model you want: the stack is sequential, the heap is shared with future self, the kernel is the only other party. If you understand what's on the stack vs heap at any moment, you debug single-threaded bugs quickly.

WARNING

"I can't reproduce the OOM locally" is usually a sign that your local run never reached the heap-growth path. Profile under load, don't guess.

Go deeper: Bryant & O'Hallaron Computer Systems: A Programmer's Perspective chapters 7–8; strace your own program for 30 minutes and watch the syscalls.

Rung 2 — One process, many threads

Add concurrent execution inside one process. Threads share the heap but not the stack. Suddenly the program has to reason about visibility (did my write reach the other thread?), atomicity (did this read-modify-write happen as one step?), and ordering (which event happens before which?).

     ┌─────────────────────────────────────────────────────┐
     │                    PROCESS                          │
     │                                                     │
     │  ┌─────────┐  ┌─────────┐  ┌─────────┐              │
     │  │ stack A │  │ stack B │  │ stack C │              │
     │  └────┬────┘  └────┬────┘  └────┬────┘              │
     │       │            │            │                   │
     │       └────────────┼────────────┘                   │
     │                    ▼                                │
     │             ┌─────────────┐                         │
     │             │ shared heap │   ← races live here     │
     │             └─────────────┘                         │
     └─────────────────────────────────────────────────────┘

The model you want: every shared mutable word is a potential race. The fix menu is short: make it immutable, make it thread-local, or put a lock around every access path. The language's memory model (Java's happens-before, C++'s sequential consistency, Go's race detector) is the contract between you and the hardware.

CAUTION

A missing volatile or memory barrier can make a bug appear only on ARM, or only after the JIT has optimized the loop. Reproducing intermittent concurrency bugs on one architecture is not enough.

Go deeper: Goetz Java Concurrency in Practice chapters 2–5; the CPython GIL deep-dive by David Beazley; run ThreadSanitizer over your own C++ code.

Rung 3 — One host, many processes

Spawn a second process. Now you need IPC — pipes, shared memory, Unix sockets, signals. Processes don't share memory by default, which is both a safety win (no data races across processes) and an efficiency loss (every message is a copy unless you use shared memory).

Rendering diagram…

New failure modes:

  • Zombies — child exited, parent didn't wait. Fills the process table.
  • Orphans — parent died, child reparented to init. Runs forever unless it watches getppid().
  • IPC stalls — pipe buffer full; writer blocks forever because reader died quietly.

The model you want: IPC is slower than memory access but safer than shared memory. When you need speed, shared-memory plus a lock. When you need safety, messages plus careful draining.

TIP

Unix sockets (AF_UNIX) are 2–3x faster than loopback TCP for local IPC and give you the same API. Use them.

Go deeper: Stevens Advanced Programming in the UNIX Environment chapters 15–17; build a minimal one-host job queue using only fork, pipe, and poll.

Rung 4 — Many hosts, one network

Add the network. Now you have partial failure — a message you sent may have arrived, may not have, may have been processed but the ack was lost. You cannot tell which. Every guarantee past this rung is about closing that ambiguity.

Rendering diagram…

New primitives: socket, timeout, retry, heartbeat. New failure modes: partitions (subset of hosts unreachable from another subset), black holes (traffic accepted but silently dropped), flapping (intermittent connectivity that looks like health).

The model you want: every network call has four outcomes: success, failure, timeout, and partial success you'll never be told about. Design for all four, not three.

WARNING

TCP is not a reliable messaging layer. It guarantees in-order delivery within a connection that stays up. The moment the connection drops, you're back to "did it get there?" Application-level ack, retry, and idempotency are still your problem.

Go deeper: TCP/IP Illustrated chapters 17–18; Peter Bailis's "Partitions for everyone!" talk; Aphyr's Jepsen series, even the old ones.

Rung 5 — Many services, one system

Services call services. Queues appear. The system becomes a graph, and failures propagate along edges.

Rendering diagram…

New primitives: message queue, consumer group, idempotency key, circuit breaker, backpressure. New failure modes:

  • Cascading failure — Svc3 slows down, Svc1's threads pile up waiting, Svc1 becomes unhealthy, Gateway's retries make it worse.
  • Retry amplification — upstream retry policy × downstream retry policy × consumer retry policy = 1000x load on the thing that's already sick.
  • Ordering bugs — events delivered out of order because queue partitions changed, because a consumer was rebalanced.

The model you want: every cross-service call is a possible timeout, every timeout is a possible retry, every retry must be idempotent. The Foundations handbook covers these primitives formally; this rung is where you feel them.

CAUTION

Circuit breakers without half-open probes turn transient failures into permanent outages. Bulkheads without tuning turn one slow dependency into starving all callers. These patterns have sharp edges — read the docs.

Go deeper: Nygard Release It! second edition in full; the Netflix Hystrix wiki (Hystrix is dead, the vocabulary survived); the Amazon Builders' Library article on timeouts, retries, and backoff.

Rung 6 — Many regions, one product

Put hosts in multiple regions. Now the physics changes: cross-region RTT is 50–200 ms, and WAN links fail more than LAN links. State must be replicated, and replication makes you confront consistency head-on.

  us-east-1              eu-west-1                 ap-south-1
  ┌────────────┐         ┌────────────┐            ┌────────────┐
  │  Primary   │◀───────▶│  Secondary │◀──────────▶│  Secondary │
  │   DB       │  async  │    DB      │   async    │    DB      │
  └─────┬──────┘         └─────┬──────┘            └─────┬──────┘
        │                      │                         │
  ┌─────┴──────┐         ┌─────┴──────┐            ┌─────┴──────┐
  │  Services  │         │  Services  │            │  Services  │
  └────────────┘         └────────────┘            └────────────┘
       ▲                       ▲                         ▲
       │                       │                         │
       └─── DNS / Anycast ◀────┴──── geo-routed ─────────┘

New primitives: consensus (Raft, Paxos), leader election, multi-region write strategies (active-active, active-passive, leader-per-key), snapshot isolation, conflict-free replicated data types (CRDTs). New failure modes:

  • Split brain — a partition makes two regions each believe they're the leader. Both accept writes. Reconciliation is hell.
  • Stale reads — you read a region that hasn't caught up yet. "I just saved this — why don't I see it?"
  • Asymmetric traffic — one region's users generate writes that the others have to apply asynchronously; failover moves write traffic to a region not provisioned for it.

The model you want: every replication strategy trades between latency, consistency, and availability along exactly the axes CAP/PACELC names. You don't escape the tradeoff; you pick which side you're optimizing.

WARNING

Multi-region active-active for a transactional workload is the single biggest "looks easy, isn't" in modern architecture. If you can draw the diagram, congratulations — you've done approximately 3% of the work. Start single-region and earn the multi-region move; don't pay the tax by default.

Go deeper: Kleppmann DDIA chapters 5 and 9; the Spanner paper; Patterns of Distributed Systems by Unmesh Joshi; any Jepsen report on a database you actually run.

How the rungs connect

Climbing is superset-style: each rung inherits everything below plus a new failure mode. You cannot skip rungs in production — you pay for the primitives of the rung you're on, whether or not you understand them.

Rendering diagram…

A region-replicated service with a race condition on one host is still a buggy service. The rung framing is not an excuse to ignore the lower rungs; it's a reminder that failure modes accumulate.

Standards & Specs

Test yourself

Your single-region service gets a new requirement: customers in Europe see 300 ms latency. Which rung does this problem live on, and what do you not need to do?

The problem is on Rung 6 (regions), but the solution often lives on Rung 4 or 5 — an edge cache or a read replica may be enough. You don't necessarily need multi-region writes; many "global" products are global reads with regional writes. See also the Cloud & Infrastructure handbook on edge caching.

A load test on a new service shows p99 latency climbing as concurrency rises, then suddenly flattening. What rung and what primitive?

Rung 5. The flattening is backpressure: the queue filled and the upstream is now rate-limited. Without a bound you'd have seen memory growth and eventual OOM instead. See Foundations Station 7.

Your team proposes "switch to microservices" to solve a bug that corrupts data under concurrent edits. Why won't it help?

The bug is on Rung 2 (many threads, shared state). Microservices add Rung 5 primitives and failure modes but don't remove the underlying race — unless you also move the shared state into a single-writer service, which is a design change, not an architecture style. Diagnose the rung before picking the remedy.