Capstone·19 min read
How a Backend Is Built Around One Message
Ten backend pieces, one chat message, and the exact thing each piece is the only one that can do.
Big Idea
Servers, APIs, databases, caches, WebSockets, queues, load balancers, containers, architecture, observability. Drawn as a stack they look like the parts list for a real backend. They are not a list, they are ten answers to ten different questions, and one message moving from one phone to another asks all ten in order. Nadia types running ten minutes late and hits send. Everything below is what has to exist for Theo to read it, and nothing below exists until the version before it runs out.
The Shape Of It
Why The Phones Can’t Just Talk To Each Other
The obvious design is the one that does not work: Nadia’s phone handing the message straight to Theo’s. Phones make terrible delivery targets. Neither one has an address the other could dial, both get a new one every time they move between wifi and cellular, and Theo’s is locked in a pocket most of the day.
So neither phone tries. They both talk to one machine that is none of those things, and that machine is the entire backend on day one.
Interactive
Watch The Backend Grow, One Piece At A Time
One canvas, ten pieces. Press play, or step through it. Nothing ever moves, it only arrives, and the badge on each box is the piece that put it there.
The Walkthrough
Ten Pieces, And What Each One Charges
Worked Example
Asking, Versus Being Told
Theo’s app has to find out the message exists. Both versions below work. The difference is who speaks first, and that one change removes the empty round trips and the delay at the same time.
Polling: the client asks, over and over
WebSocket: the server speaks first
Worked Example
The Locked Phone, And Whose Outage It Is
There is no socket, because Theo’s app is not running. Reaching him means calling Apple or Google, which means a request to a company you do not operate. Leaving that call on the request path hands them your send button.
Inline: Nadia waits for Apple
Queued: the slow dependency moves off the request
Worked Example
Ten Servers, And The One Holding The Socket
One machine runs out of memory and file descriptors long before it runs out of users, because every socket is held for hours rather than milliseconds. Ten machines fix that and break delivery: a connection lives on exactly one process, and the server handling Nadia’s send is almost never the one holding Theo’s socket.
The load balancer does not help here. It decides where new connections go, and has no way to write into one that is already open somewhere else.
Worked Example
Thirty Seconds Late, And Every Dashboard Green
Messages are arriving half a minute after they were sent. API p99 is 60ms, the error rate is zero, every service reports healthy, and all of that is true. Per-service metrics measure components answering for themselves, and nothing was slow inside a component. The message was waiting between them, and a queue is not a service with a latency number.
A trace follows the one message across every hop and puts the time where it actually went:
Recap
The Whole Path
Read straight down. Each line is what that piece does, and the line between is what it still could not do — which is the piece underneath it.
Key Points
What To Carry Forward
Each piece answers exactly one question: where does the code run, what may be asked, what survives a restart, what repeats, who gets told, what can wait, which machine, which environment, which codebase, where did the time go.
A database makes a message durable and a cache makes a read cheap. Neither of them delivers anything — delivery is the socket's job, and that separation is the whole reason both exist.
Polling and WebSockets both work. One pays per empty check, the other pays by holding state, and that state is what stops an app server from being freely restartable.
A queue does not make the push notification faster. It moves it off the path the sender is waiting on, and buys retries, duplicates, and a message that is 'sent' before it has been sent.
The moment there are two servers, delivery becomes routing: a socket lives on exactly one process, so every other server needs a channel to reach it.
Observability is the only piece that adds no capability at all. It is what makes the other nine debuggable, which is why it is the one that should never be last.
Quiz Review
Check your understanding
Question 1 of 6
A chat app keeps messages in a list in memory and delivers them over WebSockets. Everything passes in testing. What does the first real deploy break, and which piece fixes which half of it?
- ✓Both halves of the same restart. The list in memory is gone, which deletes every conversation on the service, and that is what a database fixes: the data outlives the process. The open sockets are also gone, and no storage layer helps there, because a socket cannot be persisted. That half is fixed on the edges of the restart instead: drain connections before the process stops, and have the client reconnect with backoff and ask for anything it missed while it was disconnected. Durability and connectivity look like one bug here and are two different pieces.