Books Deep-Read · DDIA · Chapter 4

Encoding and Evolution

Designing Data-Intensive Applications · Ch 4 · Martin Kleppmann · 2017

中文 →

What is this chapter about?

The app on your phone updates almost every week — yet the post you made last year, the order you placed three years ago, still has to show up correctly today. Hidden in there is a quiet problem: the code changes constantly, but old data has to stay readable. This chapter is about exactly that: data lives as living objects in memory, but the moment it goes to disk or onto the network it must be "flattened" into a stream of bytes — and when the code gets upgraded and the shape of the data wants to change too, how do you keep the new and old generations able to read each other's writing?

An analogy first

Think of "storing and sending data" as shipping a parcel: the stuff spread on your desk (objects in memory) can't just go in the mailbox — you first pack it into a box, and that step is called encoding; the recipient unpacks and restores it, called decoding. The catch: sender and receiver aren't using the same instruction manual. Your app already updated and slipped something new into the box; their app is still the old version, and its manual has no entry for it. How do you keep the old version from being stumped on arrival? That is the whole drama of this chapter.

Why this is so awkward

Because big systems can't just stop and swap everything at once. Upgrading hundreds or thousands of servers can only be done a few at a time (a rolling upgrade) — so for a stretch of time, new-version and old-version code are running side by side. Data the old code wrote must be readable by the new code (call it backward compatibility, honoring the past); the reverse is trickier: data the new code writes must also be readable by the old code, even with fields it has never seen (call it forward compatibility, honoring the future). Data often outlives the code — a record filed away five years ago is still sitting in the database, read by several later generations of code. Get compatibility wrong and every upgrade becomes a buried landmine.

The trick: give every field a "luggage tag"

The clever formats (Google's Protocol Buffers, Facebook's Thrift) use one plain, effective move: they identify fields not by name but by a number tag — like airport baggage, where what's read is the number on the tag, not what your suitcase looks like. The stored bytes contain only "tag N: this value," with no long field names. So: to add a new field, hand out a fresh, unused number; old code that hits a tag it doesn't recognize just shrugs and skips it, never crashing. Adding fields without collisions rests entirely on that tag. The one iron rule: once a tag number is handed out, it can never be changed or reassigned to someone else.

The three places data changes hands

Data flows from one piece of code to another by just three routes, each of which must pass the compatibility test: ① Into a database — the code that writes it and the code that later reads it may be several versions apart; ② Calling a service — a phone app sends a request to a backend, and the two sides upgrade independently; ③ Through a message queue — one system drops a message into a pipe and another picks it up later, so send and receive aren't even at the same moment. As long as the two ends might be on different versions, the encoding format has to carry the burden of keeping them mutually intelligible.

Remember this one thing

Data must be packed into bytes (encoded) before it leaves memory and unpacked (decoded) when it comes back. The hard part is that code is rolling-upgraded and new and old versions run at once, so the format must let new code read old data and old code survive new data. The way through: identify fields by number, not name — add fields with fresh numbers, never reuse an old one, and upgrades stop being landmines.

Want the concrete formats, how the bytes lay out, how real systems use them? → Switch to the deep read