Your API Contract Is a Promise. Stop Breaking It.
by Arif Ikhsanudin, Backend Developer
The Breaking Change Nobody Announced
Your team renamed a JSON field from userId to user_id to match your new naming convention. The change was "internal," and anyway the consumer team would update their code — they were CC'd in the PR. Except they weren't actively monitoring that PR. Their service went to production two days later, calling your API, and started failing silently because userId now returned null. The bug took four hours to trace.
This is a breaking change. Not a controversial one, not an edge case — by any definition of the term. userId existed in your contract and now doesn't. Every caller depending on it is broken.
The fact that it was unintentional doesn't change the consequence.
What Constitutes a Breaking Change
Breaking changes are more numerous than most teams recognize. They are not limited to removing fields or changing endpoints. A breaking change is any change that causes a valid request that previously worked to either fail or produce a different result.
Backward-incompatible changes include:
- Removing a field from a response
- Renaming a field (even to a "better" name)
- Changing a field's type (string to integer, nullable to required)
- Adding a required field to a request body
- Changing the semantic meaning of an existing field
- Removing an enum value that callers may be matching against
- Tightening validation (rejecting inputs that were previously accepted)
- Changing error codes that callers may be handling
Backward-compatible changes (safe to make without a version bump):
- Adding an optional field to a response
- Adding an optional field to a request body
- Adding a new endpoint
- Adding a new enum value (callers must handle unknown values gracefully)
- Relaxing validation (accepting inputs previously rejected)
Versioning Strategies
There are three common approaches, each with real tradeoffs:
URI versioning (/v1/orders, /v2/orders): Explicit, visible in logs, easy to route. Leads to proliferation of versioned endpoints that need to be maintained in parallel. The most common approach for public APIs.
Header versioning (Accept: application/vnd.myapi.v2+json or a custom API-Version: 2 header): Keeps URIs clean. Harder to test manually (must set headers on every request), less visible in logs. Common in REST-purist environments.
Query parameter versioning (/orders?version=2): Simple to implement. Generally considered a weaker signal of intent — query parameters are supposed to filter, not route.
Pick one and be consistent. Mixing strategies in the same API is worse than any one strategy imperfectly applied.
The Deprecation Protocol That Actually Works
A contract isn't just about new versions — it's about how long old versions live. A deprecation protocol removes ambiguity:
- Announce deprecation with a concrete end-of-life date — not "sometime next quarter"
- Add a
Deprecationresponse header and aSunsetheader (RFC 8594) on every response from the deprecated endpoint:Sunset: Sat, 31 Dec 2026 23:59:59 GMT - Monitor call volume to the deprecated endpoint. If it's still at 10% of its peak at the end-of-life date, the date may need to move
- Give consumers a migration guide, not just a changelog
The Sunset header is machine-readable. Clients can programmatically detect that an endpoint is being retired and alert their teams. This is better than email threads.
Consumer-Driven Contract Testing
The most robust technical approach to preventing breaking changes is consumer-driven contract testing with Pact. Here's how it works:
- Each consumer service defines the requests it makes and the response fields it depends on (not the full response, just the parts it uses)
- These "pacts" are published to a Pact Broker (a service that stores and manages contracts)
- The provider's CI pipeline verifies that the current provider code satisfies all published pacts before merging or deploying
Consumer A pact: GET /orders/{id} must return { id, status, totalAmount }
Consumer B pact: GET /orders/{id} must return { id, customerId }
Provider CI: run pact verification -> confirm both contracts are satisfied
If you rename totalAmount to total, Consumer A's pact fails in the provider's CI pipeline before the change reaches production. You catch the breaking change before it breaks anything.
This is the correct answer to "how do we prevent accidental breaking changes in a microservices environment."
The Practical Takeaway
Before your next API change, run through the breaking change checklist above. If any item applies, you have two options: create a new API version or find a backward-compatible way to make the change. "We'll notify them" is not a third option — it's a process that will fail at least once and cause a production incident. The tooling exists to do this correctly. Use it.