Skip to main content

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

Andrews Ribeiro

Founder & Engineer

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 status always present?
  • can new values appear?
  • does 404 mean “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 amount and currency, 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

Practice checklist

Use this when you answer

You finished this article

Next article How to Design a Third-Party Integration Without Becoming a Hostage Previous article Your REST API Was Almost Never REST

Keep exploring

Related articles