February 28 2025
Service Contracts and Backward Compatibility
How to evolve internal integrations without treating another team's consumer as if it were just a detail of your deploy.
Andrews Ribeiro
Founder & Engineer
4 min Intermediate Systems
The problem
Some teams treat internal integration as if it were a private conversation.
Something like:
“If I change this and it breaks, the other team can adjust quickly.”
On paper, that sounds efficient.
In practice, it creates the classic system where every deploy has a chance to leak trouble into another service.
The reason is simple:
- one service publishes behavior
- another service starts depending on it
At that moment, a contract was born.
Even if nobody called it that yet.
Mental model
A service contract is the real agreement between producer and consumer.
That agreement includes:
- fields
- types
- requiredness
- semantics
- status codes
- event order or format
- error rules
Backward compatibility means evolving that agreement without breaking the people still depending on the previous shape.
In plain language:
the new service version is still understandable to the old consumer
Breaking the problem down
Schema is only one part
Many contract conversations stop too early at the schema.
But a contract is not only this:
{
"id": "123",
"status": "paid"
}
It is also expectations like:
- is
statusalways present? - can new values appear?
- does
404mean “does not exist” or “you cannot see it”? - can the event arrive duplicated?
If those answers are not clear, the contract is still weak even with a “pretty” schema.
What is usually compatible
Changes that are often safe:
- adding an optional field
- accepting a new value without invalidating the old ones
- adding a new endpoint or event
- enriching a response without changing previous meaning
They tend to work because the old consumer still understands what it already knew.
What usually breaks
Dangerous changes:
- removing a field that consumers use
- renaming a field
- changing a type
- making something required when it was not before
- changing the meaning of an existing value
- changing error behavior without transition
The problem is almost never the code line itself.
It is broken expectation on the other side.
A strong producer thinks in coexistence
When an important change comes, a mature producer thinks in transition:
- add the new field or behavior
- keep the old one for a while
- communicate deprecation
- measure who still depends on it
- remove only after migration
That applies to HTTP APIs, events, and queue-based integrations.
A strong consumer also protects itself
Compatibility is not only the producer’s responsibility.
A strong consumer avoids assuming too much:
- ignores extra fields
- tolerates accidental lack of ordering when the contract does not promise ordering
- does not couple parsing to useless detail
- handles unknown values with safe degradation when that makes sense
If a consumer breaks because one extra field appeared, it is fragile too.
Simple example
Imagine an order service that returns:
{
"id": "ord_1",
"status": "paid",
"total": 150
}
Another service uses that to issue an invoice.
Now the producer team wants internationalization and changes total into:
{
"total": {
"amount": 150,
"currency": "BRL"
}
}
To the producer, that may look like evolution.
To the old consumer, it may be a direct break.
A better transition would be:
- keep
total - add
amountandcurrency, or a new object in parallel - observe usage
- remove later with an agreed window
The strong move is not “keep legacy forever.”
It is replacing silent rupture with controlled evolution.
Common mistakes
- Calling it “internal” and using that as an excuse to break the contract.
- Assuming a valid schema alone guarantees compatibility.
- Changing semantics while keeping the same field name.
- Removing the old field as soon as the new consumer is ready.
- Not measuring who still depends on the old shape.
How a senior thinks about it
Someone with more experience treats service integration as a product boundary, even inside the same company.
The reasoning usually sounds like this:
“That other service is my operational customer. If I change the agreement without transition, I export risk to them.”
That way of thinking reduces human and technical coupling at the same time.
Less deploy war. More predictability.
What the interviewer wants to see
In interviews, this usually appears in API evolution, events, or internal integration discussions.
The evaluator wants to see whether you understand that a contract is a real commitment.
You level up when you:
- distinguish schema from semantics
- talk about transition and deprecation
- mention usage observability
- show that the consumer also needs to be robust
A strong answer often sounds like this:
“I would treat the service contract as a stable agreement. First I try compatible evolution. If the change is breaking, I create temporary coexistence, measure usage, and remove later.”
Over-coupled systems do not fail only because teams are bad. They fail because the contract was treated like a detail.
Quick summary
What to keep in your head
- A contract is not only field names. It also includes format, semantics, requiredness, and expected behavior.
- Backward compatibility reduces operational coupling between producer and consumer.
- A change that looks small to one service may still be breaking to another if the agreement was implicit.
- Safe evolution depends on adding before removing, observing usage, and making transition explicit.
Practice checklist
Use this when you answer
- Can I explain what turns a change into a breaking one for another service?
- Do I know the difference between a valid schema and a truly compatible contract?
- Can I describe a transition strategy between producer and consumer?
- Can I talk about compatibility without reducing everything to a version number?
You finished this article
Next step
API Versioning in Practice Next step →Share this page
Copy the link manually from the field below.