Lesson 01 · The founding problem

The unreliable network: where all the trouble starts

Every hard thing in distributed systems descends from one physical fact: between two machines there is a network, and the network is allowed to do anything. Drop a message. Delay it ten seconds. Deliver it twice. Deliver it after you've given up. And crucially — when you send a request and hear nothing back, the network offers you no way to distinguish "the request was lost," "the server crashed," "the server is slow," and "the server did the work but the reply was lost." One symptom, four realities.

Before building anything on such a foundation, it's worth knowing what's provably impossible on it. The 1975 result that opens every textbook:

Two generals, one unreliable valley

Two armies must attack a city simultaneously or lose. They coordinate by messengers who cross a valley where they may be captured. Can the generals reach certainty that both will attack? Try:

Achieve certainty (you can't)

Send messengers back and forth. Each arrival helps — and creates a new uncertainty of its own. Follow the "who's sure of what" tracker and try to get both generals to certain.

GENERAL A proposes: dawn attack GENERAL B knows nothing yet the valley (messengers may vanish)
messengers sent
0
arrived
0
captured
0

The proof is the pattern you just felt: whatever the last successfully delivered message was, its sender can't know it arrived — so its sender's willingness to attack must not depend on it. Induct backwards and no finite number of messengers produces common knowledge. The engineering moral is liberating, not depressing: certainty over an unreliable channel is off the menu, so real systems buy probability instead — acknowledgements, retries, timeouts — and design so that the residual uncertainty is harmless rather than absent. Which brings us to the harm:

"Did my payment go through?"

A client charges a card. The request times out. The client is now standing exactly where General A stood — and what it does next separates correct systems from incident reports:

Charge €50, lose the reply, retry

Each attempt randomly: arrives & acks, arrives but the ack is lost, or never arrives. On timeout, retry — first naively, then with an idempotency key. Watch the server's ledger, not the client's beliefs.

client idle payments API listening
attempts
0
acks seen by client
0
CHARGES ON SERVER
€0
server ledger — empty

What the toggle changes is profound. Without it, the server treats every arrival as new work: retries are dangerous, so timeouts become paralyzing. With it, the server keeps a small table — "key ord_7f3a: already processed, result was OK" — and duplicate arrivals return the stored result instead of re-executing. Retrying is now safe, which means the client can retry aggressively, which means the unreliable network stops being scary. The formula worth memorizing: exactly-once effect = at-least-once delivery + idempotent processing. Nobody delivers exactly once; correct systems process as if they did.

Idempotency in the wild: Stripe requires an Idempotency-Key header on POSTs for precisely this reason; DynamoDB conditional writes, S3 PUTs of the same object, and UPSERTs are naturally idempotent; SQS/Kafka consumers deduplicate by message ID. And note the quiet prerequisite: the client must generate the key before the first attempt — a key invented at retry time identifies nothing.

Timeouts: the least-bad guess

One loose end from the lab: how long should the client wait before retrying? There is no correct answer, only a trade. Too short, and you retry against a server that's merely slow — duplicating work and adding load exactly when the system is struggling (Lesson 6 shows this becoming a self-inflicted outage). Too long, and users stare at spinners while a genuinely dead server wastes their time. Real systems pick timeouts from observed latency percentiles (e.g., p99 × a small multiplier), adapt them continuously, and — because a timeout is a guess, not a fact — pair every retry with the idempotency machinery you just built. The three-legged stool of this lesson: timeouts to suspect, retries to persist, idempotency to make persistence safe.

Check yourself

1. A message-queue vendor advertises "guaranteed exactly-once delivery." What's the accurate reading?

Two Generals is the proof: an unreliable channel can't establish certain one-time delivery. Systems marketed as exactly-once (Kafka transactions, for example) deliver at-least-once under the hood and make duplicates invisible via producer IDs, sequence numbers, and transactional consumers — exactly the lab's idempotency key, industrialized.

2. Your service calls a dependency; the call times out. Which statement is true at that moment?

A timeout is the absence of information, not information. Every correct pattern downstream — idempotent retries, reconciliation jobs, at-least-once queues — exists because the timed-out caller must plan for both possibilities simultaneously.

3. Why must the idempotency key be generated by the client before the first attempt, rather than by the server?

If the server assigns IDs on arrival, two arrivals get two IDs and the duplicate is undetectable. The client minting ord_7f3a before attempt #1 is what makes attempt #2 provably the same intent. This tiny protocol detail is the entire difference between the lab's single and double charge.