Versioning APIs and events carefully
coreintermediateAn API or an event schema is a contract between whoever produces it and every consumer that reads it, and unlike a function inside one codebase, that contract cannot be changed and redeployed everywhere at once — a mobile app version from six months ago, a partner's integration, and an internal service that has not been redeployed yet may all still be calling the old contract long after a new one ships. Versioning is the discipline of managing that reality deliberately rather than assuming every consumer upgrades in lockstep with the producer. The safest changes are additive and backward-compatible — adding a new optional field, adding a new endpoint — which every existing consumer can simply ignore without being broken. A breaking change (removing a field, changing a field's type or meaning, changing required parameters) needs an explicit versioning strategy: a new version number in the URL or a header, coexisting for a defined deprecation window with the old version, so consumers migrate on their own schedule rather than breaking the instant the change ships. The same discipline applies to events published onto a queue or log (Kafka, SQS): a consumer reading an event schema has the same lockstep problem as an API caller, except often with less visibility into who all the consumers even are, since a queue or topic can have consumers a producer does not know about at all.
Think of it as
Think of a public library revising the layout of its card catalog. If it only adds new categories (additive, backward-compatible), a patron's decades-old habit of looking up fiction under "F" still works exactly as before, plus new categories exist for people who want them. If the library removes the "F" section entirely and reorganizes everything under a new scheme overnight (a breaking change with no versioning), every patron who walks in the next day using their old habit is lost — the library has broken a contract patrons had no way to know was about to change and no time to adapt to. A well-versioned change is the library posting "the fiction section is moving to the new wing on the first of next month, both layouts work until then" — giving every patron, on their own visit schedule, a window to adjust.
What we're doing: Compare shipping a breaking field-type change with no versioning versus with a proper deprecation window.
- 6
- This is the failure mode versioning exists to prevent: every consumer that assumed the old shape breaks simultaneously, at a time chosen entirely by the producer, with no ability for any individual consumer to control when it is affected.
- 13
- Every consumer migrates on its own schedule within the window — a mobile app that only gets a new release every few weeks and an internal service that redeploys daily both have a real path to migrate without either one being broken in the meantime.
Why this works: The underlying change (total's type) is identical in both cases — the difference is entirely in whether consumers were given a contract that let them adapt, and that difference is the entire practical value of a deliberate versioning strategy over an ad hoc "just ship the improved shape" approach.
Treating a field-type change as safe because "most clients probably don't care"
Wrong
Better
What you see: A breaking change shipped without versioning "because most consumers would be fine" breaks a partner integration that had not been touched in months, and the partner discovers it only when their own customers start reporting errors — well after the change already shipped.
Why: "Most clients probably don't care" is a judgment about the clients a team knows about and is thinking of at the moment of shipping — it says nothing about clients that are less visible (an old partner integration, a cached mobile app version still in the wild, an internal service nobody remembered still calls this endpoint), which are exactly the ones a versioning strategy protects without requiring the producer to have complete visibility into every consumer.
- T+0: v2 released — v1 continues unchanged; both live side by side
- T+0 to T+90d: Deprecation window — consumers migrate to v2 on their own schedule
- T+60d: Sunset warning — v1 responses include a deprecation header
- T+90d: v1 retired — only after the documented window elapses
Change type, and whether it needs a version bump
Remember: Additive, backward-compatible changes need no version bump — existing consumers can ignore what they do not recognize. A breaking change needs an explicit new version, served alongside the old one for a defined deprecation window, so every consumer — including the ones a producer does not have full visibility into — migrates on its own schedule rather than breaking the instant the change ships. This applies to events on a queue or log exactly as much as to a request/response API.
See also: backward compatible database changes · event schema evolution and consumer compatibility · versioning and backward compatibility

