DEV Community

M. Alwi Sukra
M. Alwi Sukra

Posted on

TIL - Compatibility Direction in Schema Evolution

This week I read DDIA Chapter 4, on encoding and evolution, and the part that stuck with me was how to reason about schema compatibility during a rolling upgrade.

In a large system, code changes don't happen all at once. We do a rolling upgrade, so old and new versions of the code run side by side for a while. For the system to keep working through that window, the data has to survive being read by whichever version happens to pick it up. That's what forward and backward compatibility are for.

Which direction we actually need isn't fixed. It depends on the dataflow, and on the order we roll things out.

Through a database

Say we add a field and update the code that reads and writes the row.

Backward compatibility is obviously needed: data written by the old code shouldn't break when the new code reads it. But forward compatibility is needed too, because during a rolling upgrade an old instance can read a row that a new instance already wrote.

And there's a twist a database adds that services don't have: old data just sits there. A row written two years ago is still there, in the old shape, and we have to keep supporting it. We could migrate (rewrite) the data to avoid that, but on a large database that's expensive, so most of the time we don't. So the database case needs both directions, and it needs them for a long time.

Through services (REST and RPC)

Now say we change a request or response schema. Here it splits by who's processing what.

On the server side, we need backward compatibility: the new server code has to handle old requests from clients that haven't upgraded yet. What we usually don't strictly need is forward compatibility, because we always deployed the server fully first, then told clients to upgrade. So an old server never sees a new request. I'd been doing this by instinct; the chapter just gave me the reason it holds, the rollout order is what makes that direction safe to skip.

On the client side it's the mirror image. The client needs forward compatibility: old client code has to handle new responses, because the server upgraded first and is already sending the new shape. It doesn't need backward compatibility, because by the time the client upgrades, the server is already new.

So "the direction we care about" was never really a rule about APIs. It was a consequence of how we rolled out. Server first, clients after.

Through message passing

Producer and consumer versions coexist, and we can't force the order the way we can with server-then-client. A newer consumer might read a message from an older producer, and an older consumer might read a message from a newer producer. So the consumer needs both directions.

And even if we decide to upgrade the producer fully first, we still can't drop forward compatibility, because messages live in the broker. They sit there for retries and replays, so a message written by the new producer can still be waiting when an old consumer picks it up. We can't tell a message already sitting in the queue to upgrade itself.

That's the difference. With an API, the rollout order is something we control. With a queue, the message outlives the moment it was written, so the order isn't ours to enforce anymore.

The summary

Backward (old data → new code) Forward (new data → old code)
Database Yes Yes
Service (server) Yes No, because server upgrades first
Service (client) No, because server upgrades first Yes
Message passing Yes Yes

The thing I took from it: I'd been handling both directions in practice, but as a set of habits, not a clear model. What this chapter gave me was the definition, backward and forward as two independent directions, and the reason the direction we need shifts with the dataflow and the rollout order. The moment the dataflow stops letting us control that order, like a message queue does, the shortcut of leaning on rollout order stops working, and we have to actually support both.

Top comments (0)