Backwards compatibility is the job
There's a promise in enterprise SaaS that nobody writes into the contract and everybody enforces anyway: the integration shipped three years ago still works today. Not "works if they upgrade." Not "works with a small migration." Works — untouched — while half the system underneath it has been rewritten.
Consumer software gets to break things. Ship a redesign, users grumble for a week, they adapt. Enterprise software doesn't have that door. The customer didn't just use the product — they built systems on top of it. A nightly job that parses the CSV export. A middleware that maps the webhook payload into their ERP. An integration written by a contractor who left in 2023, that nobody dares touch, that finance depends on to close the month. When the shape of a field changes, none of that grumbles and adapts. It breaks, silently, in their production, and the first anyone hears of it is an escalation with the vendor's name on it.
A decade building enterprise multi-tenant SaaS inverts the instinct picked up in startups — move fast, refactor freely, the schema is the team's own. The contract is not the team's to change. It belongs to everyone who integrated against it, and the job is to keep evolving the product without ever breaking the promise underneath them.
The contract is bigger than the API
The first mistake is thinking "the contract" means the documented API. It's much larger than that. It's every observable behavior a customer could have come to depend on — and they depend on far more than was ever documented.
This is Hyrum's Law, and it's the law that governs the whole discipline: with enough users, every observable behavior of the system gets depended on by somebody — regardless of what was documented. The order fields come back in. The exact text of an error message someone regex-matched. A list that happened to be sorted so they never added their own sort. The null sent where they now expect null. The 200ms of latency they built a timeout around. Even bugs: fix one that a customer quietly worked around, and the fix is their breakage. The documented API is the small green box. The thing that actually can't be broken is the whole diagram.
Additive is safe; everything else costs
The load-bearing distinction is between changes that add and changes that alter or remove. Adding is almost always safe, because a well-built consumer ignores what it doesn't know about. Altering the shape of something that's already there is where integrations break.
// SAFE — additive. Old integrations don't see it; nothing they parse moved.
{ "id": 42, "status": "active", "created_at": "…", "tier": "enterprise" }
// ↑ new field, added at the end
// BREAKING — every one of these silently breaks a consumer somewhere:
// • rename status → state
// • change "active" → "ACTIVE" (enum value)
// • change id: 42 → id: "42" (type)
// • remove created_at
// • reorder fields a positional parser relied on
Internalize that table and most day-to-day compatibility takes care of itself: add columns, don't rename them; add enum values carefully (a new status can still surprise a consumer that switch-matched the old set, so even additions have edges); never change a type or drop a field a customer can see. The moment a shape genuinely needs to alter, don't do it in place — reach for the two disciplines below.
Expand, migrate, contract
The pattern that lets a team change anything without a breaking moment is parallel change — expand / contract. Never flip old to new. Run both, move consumers across on their schedule, and only remove the old thing once nobody's on it.
The other lever is versioning — a new major API version (/v2, or a version header) lets a team make a clean break for new integrations while /v1 keeps its promise to the old ones. It works, but the cost is real: every version published is a version to maintain, test, and secure until the last customer leaves it — and enterprise customers leave slowly. Versioning isn't an escape from compatibility; it's compatibility with a bounded blast radius and a standing maintenance bill. Prefer additive evolution within a version; reserve a new version for changes that genuinely can't be made additively. Semantic versioning is the shared vocabulary for signaling which is which.
Retiring things — carefully
Compatibility forever doesn't mean nothing can ever be deleted; it means nothing can be deleted by surprise. Retirement is a process with four non-optional parts, and the first is the one teams skip:
- TELEMETRY
- Nothing can be retired that can't be measured. Per-endpoint, per-field usage by tenant — so "is anyone still using this?" is a query, not a guess. Without it, every deletion is a gamble on a forgotten customer.
- RUNWAY
- Measured in quarters, not sprints. Enterprise change calendars are slow; a 30-day deprecation is a broken promise with a countdown on it.
- SIGNAL
- Deprecation notices in docs, changelog, and in-band — a
Sunsetheader, aDeprecationwarning — so an integration surfaces the end date to the humans who own it, before it bites. - EXCEPTIONS
- Sometimes the answer for the biggest account is "never." A contract worth more than the maintenance is a contract worth keeping the old path alive for. That's a business call, made with data — not an engineering shortcut.
What to wire in
Same instinct the whole series runs on: make the system catch the breaking change, so compatibility doesn't depend on every engineer remembering Hyrum's Law at 5pm on a Friday.
- 01
A schema-diff CI gate
Diff the API/response schema against the last release on every build; fail the pipeline on a breaking change (removed field, changed type, narrowed enum) unless it's an explicit, reviewed version bump. Turns "we hope nobody broke it" into a red build.
- 02
Consumer-driven contract tests
Let integrators' expectations run as tests against the build — tools like Pact encode "what the consumer actually depends on" so a break shows up in CI, not in their production.
- 03
Per-endpoint usage telemetry
Who calls what, which fields they read, when they last did. The prerequisite for ever safely deleting anything — and the evidence behind every deprecation decision.
- 04
An expand/contract migration playbook
A written, repeatable path for shape changes: expand, dual-write/dual-read, migrate consumers, verify zero old usage, contract. So the safe way is the default way, not the heroic exception.
Moving fast is a startup virtue. In enterprise SaaS the virtue is moving without anyone downstream feeling it — and that's a harder, quieter kind of good engineering.
Corrections welcome
The corrections worth having here are the war stories: the Hyrum's-Law dependency that blindsided a team, the version that can never be killed, the deprecation that took longer than the feature, the "harmless" fix that turned out to be load-bearing for someone, the breaking change that shipped anyway and what it cost. That's the knowledge that never makes it into an API style guide.
Email or X. Building in the open is partly a learning project; the outside input is the point.
— Neil
