It starts with a simple prompt
A successful checkout shows the ordinary path. Now we have to decide what happens everywhere else.
A conversation in ten chapters
What happens after
“just build it.”
Two people want it.
A small story about the decisions
hiding inside a simple request.
It starts with a simple prompt
Build a ticket website. Choose a seat, pay, get a ticket.
The checkout works. But two people want the last seat.
What should happen?
↳ A successful checkout shows the ordinary path. Now we have to decide what happens everywhere else.
01 · The reservation
Now we need: a reservation
Should selecting a seat hold it, or does the first completed payment win?
The buyer has time to pay.
abandoned checkouts block other buyers.
Unpaid seats stay available.
a buyer can lose the seat while entering payment details.
Hold it for five minutes.
Now we need expiration rules. And two simultaneous requests must not acquire the same reservation. That’s a correctness requirement once we’ve promised exclusive holds.
Database transactions can help enforce it, but their concurrency behavior matters. For example, PostgreSQL’s serializable transactions can require the application to retry after conflicts.
02 · The timeout
The customer submits payment.
The reservation expires.
No payment response yet.
Should we release the seat?
If payment failed, yes.
We don’t know whether it failed. The payment may have succeeded, but its response got lost.
The system needs an “unknown” state.
Stripe: an operation can succeed even when its response is lost
03 · The policy
The payment is unresolved.
The next buyer is waiting.
Now we need: a deadline & a remedy
While we’re waiting, which consequence should we accept?
Preserve the buyer’s seat while we find out.
everyone else may wait much longer than five minutes.
The seat is available again.
a late successful payment may need a refund.
Give pending payments two extra minutes. Then release the seat and refund any payment that succeeds later.
A reasonable policy. Now payment confirmation, reservation expiry, and refunds need precise rules—even when their updates arrive in a different order.
The words “five-minute reservation” did not specify any of that.
04 · The retry
pay_042pay_042Now we need: idempotency
If we don’t get a response, retry the payment.
Yes—but the retry must identify the same payment attempt. Otherwise we could charge the customer twice.
Payment APIs can support this through idempotency: repeat an operation using the same identifier without repeating its effect. Stripe describes this for safely retrying requests after network failures.
Fine. Use that.
Then we must preserve that identifier across retries and server restarts, associate it with the correct order, and distinguish a retry from a genuinely new purchase attempt. We still need the reservation policy while payment status is unknown.
The manager started with a button. Now they’re reasoning about which actions count as the same action, what survives a crash, and what the system knows.
05 · The backup
Now we need: a durability promise
Purchases must survive a server failure. Add a backup database.
Do we say “confirmed” before or after the backup has saved the purchase?
The primary saves it; no waiting for the backup.
a failure can leave the backup missing a confirmed purchase.
The required backup has also saved it.
confirmation waits if that backup can’t be reached.
These are actual replication trade-offs. PostgreSQL documents both the potential loss of recent transactions with asynchronous replication and the additional wait involved in synchronous replication. More replicas can improve availability, with extra resources and configuration.
Wait for the backup. We can’t lose confirmed purchases.
That decision changes checkout latency, outage behavior, infrastructure cost, and the meaning of “confirmed.”
06 · The failover
Could sell to Alice
Could sell to Ben
Now we need: one authority
If the main server stops responding, switch to the backup.
What if it’s still running, and only the connection broke? If both accept sales independently, both could sell A12.
Then make sure only one is allowed to sell.
Now the implementation needs a reliable way to establish which server has authority, and prevent the other from continuing to accept purchases.
An established database or managed service can supply that machinery. Its guarantees and failure conditions still determine what your application can promise.
“How do we know the old server has stopped accepting purchases before the replacement starts?” That is a distributed-systems engineering question. It emerged directly from “sell tickets reliably.”
07 · The system
Every request sounds reasonable. Each answer changes the requirements elsewhere.
The deeper you go, the more you’re doing engineering.
Those are engineering concepts, even when discussed without technical vocabulary.
08 · The engineering
You can delegate technical decisions. Independently evaluating them takes engineering knowledge.
The responsibility behind the code
The distinction is the ability to evaluate and take responsibility for the system’s behavior.
A developer should translate consequential trade-offs into business consequences. The manager needn’t select every mechanism.
When you start evaluating those guarantees, interactions, and evidence yourself, you’re taking on software engineering—even if an AI writes every line.
09 · The point
Those decisions
still get made.
Engineering is understanding which ones matter and establishing that the resulting system behaves as intended.
What does it promise?
What happens when it fails?
How do you know it works?