Traffic is back on the old application. Its binary starts, but a record contains a structured status where it expects text. The deployment rolled back; the data did not.
Consider a service replacing a text status with a structured object. The new writer succeeds. When an operational problem prompts a return to the old application, those records become part of the recovery problem.
Now let the object represent a state the old text field never supported, such as partially_cancelled. Copying it into the legacy field does not make the old application understand it. Mapping it to cancelled might restore parsing while changing the meaning. That is a product decision about information loss, not just a deployment fix.
The first incompatible write is the boundary. Before it, switching versions may be straightforward. After it, recovery needs a valid way to interpret or transform the new data. Deployment diagrams often highlight the traffic switch while leaving this boundary implicit.
Parallel Change, described by Danilo Sato, offers a useful pattern: expand the interface, migrate its consumers and contract it after the transition. For a stored representation, that can mean introducing a new field while retaining the old one, moving readers and writers in stages, then removing the old representation once it is no longer needed.
Writing both representations can introduce inconsistencies if one write succeeds and the other fails. A backfill can race with live updates. Keeping two fields is therefore a temporary compatibility mechanism with a consistency problem to solve, not an automatic guarantee of safety.
During the compatibility window, keep the legacy field authoritative, derive the new representation from it and postpone states the old reader cannot represent. That preserves a rollback path at the cost of delaying new behavior. If the new representation becomes authoritative sooner, rollback still needs a lossless, consistent legacy projection for every allowed state, an explicit compatibility adapter, or a stated end to the rollback promise.
Backfills need the same policy. A stale backfill must not replace a newer live update. Use a transaction, revision check or another consistency mechanism appropriate to the store; the required property is that the migration preserves the winning value, not merely that both fields exist.
Message schemas have related concerns. The Protobuf evolution guide separates changes that preserve binary compatibility from changes that are unsafe or conditional. Even a compatible field addition needs application behavior for readers that do not know the field. A successful parse cannot show that an old application handles a newly introduced state.
A rollback exercise should use data produced by the new version. Start with synthetic records, run new writes, then invoke the old reader and verify meaningful results. Merely starting the old binary proves little about its ability to resume service. Include background jobs and exported records if they also consume the changed representation.
The compatibility window should have an explicit end. Retaining every old representation indefinitely creates maintenance work and makes future changes harder to reason about. Removing it too early eliminates a recovery option. Record the conditions under which contraction becomes acceptable, including which readers still exist.
Before release, verify that old readers handle every allowed new write, derived fields remain consistent and background consumers pass the same checks. Agree when the compatibility window ends.
Test the old reader against records the new writer actually creates. The rollback promise lasts only while every allowed write remains readable with its meaning intact.