Skip to content

Avoid API Breaking Changes

Sponsor: Do you build complex software systems? See how NServiceBus makes it easier to design, build, and manage software systems that use message queues to achieve loose coupling. Get started for free.

Learn more about Software Architecture & Design.
Join thousands of developers getting weekly updates to increase your understanding of software architecture and design concepts.


Have you ever changed your backing data model and then all of a sudden all your clients explode?

It’s not because you broke your code. You broke your contract.

Most times API breaking changes occur because you’re exposing your domain model or data model. You’re not just sharing data. You’re leaking implementation details.

What you can do instead is start treating your API like an anti-corruption layer. That does not mean making seven different layers of DTOs.

YouTube

Check out my YouTube channel, where I post all kinds of content on Software Architecture & Design, including this video showing everything in this post.

Your Data Model Is Not Your API

Here’s a typical scenario with an HTTP API.

A client makes a request, you hit your database, and maybe you’re pulling that data almost exactly using some type of ORM. You take your data model and return it back to the client.

What you’ve done is leak implementation details.

That means you won’t be able to easily evolve or change your internals without causing a breaking change to your clients.

A big part of the reason for this, especially with HTTP APIs, is conflating your data model with your resource model.

Let’s say this is the JSON response from an HTTP API returning book details:

{
  "bookId": 123,
  "title": "Some Book",
  "authorId": 456,
  "date": "2025-01-01",
  "pages": 300
}

You could be using any type of read model, a document store, or a relational database. If you’re using an ORM, maybe this is almost exactly what you’re spitting back out to the client.

We have our book ID, title, author ID, date, and pages. Seems simple enough.

But the reality is that you probably want to be doing some type of composition. There’s more information the client actually needs, and you don’t want the client making a bunch of requests to get data from different sources and different parts of your system.

You probably want to do that composition yourself and return something more meaningful:

{
  "isbn": "978-...",
  "title": "Some Book",
  "author": {
    "id": "...",
    "name": "Some Author",
    "bio": "..."
  },
  "published": "2025-01-01",
  "pages": 300,
  "price": {
    "amount": 29.99,
    "currency": "USD"
  },
  "reviews": {
    "count": 247,
    "averageRating": 4.6
  }
}

I’m not returning the book ID. What the heck is that to the client? Instead, maybe there’s an identifier like the ISBN that the client actually understands.

The author is a complex object. I don’t just have an authorId sitting there flat. I have more useful information like an identifier, the author’s name, and maybe their bio.

Price and reviews might come from completely different parts of the system. That’s fine. The API is doing the composition and returning information that’s relevant to the client.

What Does This Have to Do With Breaking Changes?

Pretty much everything.

In the first example, that data model represents how I’m persisting data. I have a book ID and an author ID. Maybe those are foreign keys to other tables or references to other collections.

Our clients don’t care at all about any of this information.

They care about the composition I made and getting relevant data.

There’s a big difference between the two because if I’m not exposing my internal data model, I can iterate and change how it looks internally.

Your system is going to evolve and change over time, or parts of it will anyway. You don’t want to make changes internally that affect your clients.

That means thinking about your responses as a contract and putting a little more thought into what they look like so you’re not constantly making breaking changes.

You’ve got to leave yourself open to options.

Give Yourself Room to Evolve

Take the price from the previous example.

Maybe initially I have this:

{
  "price": 29.99
}

Is it really just the price, though? Do I also need the currency?

If I do, and I’ve exposed price as a number, now I have to figure out how to add currency without changing the structure clients already depend on.

If I put a little more thought into it initially, maybe I start with:

{
  "price": {
    "amount": 29.99
  }
}

Or maybe I have enough foresight to include the currency even if I’m only dealing with USD today.

{
  "price": {
    "amount": 29.99,
    "currency": "USD"
  }
}

The point is to think a little more about what your responses are and what the structure actually needs to look like.

These names and structures don’t need to map one to one with your data model. Nor should they.

You’ve got to come at it from the client’s perspective.

The same applies to reviews. Are reviewCount and averageRating just two random fields on a book? Or are they actually related?

Maybe this makes more sense:

{
  "reviews": {
    "count": 247,
    "averageRating": 4.6
  }
}

The same thing applies to date.

What does date mean?

In the data model, maybe everybody internally understands that it represents when the book was published. But externally, why call it date? Why not published or publishedDate?

It sounds trivial, but once clients depend on these things, you can’t just rename them. That’s a breaking change.

It Depends on Who Controls the Clients

There’s an important distinction here.

You may be in a context where you’re both the producer creating the API and the consumer using it. If you make a breaking change, you can just change your clients.

Not that big of a deal.

If that’s the case, you don’t necessarily need to put as much thought into the exact structure of your API and its responses. You can evolve both sides at the same time.

But you may be in a situation where once you release this thing to the public, there’s no going back. You don’t control the clients. You can’t just change them, and now you need some type of versioning strategy if you make a breaking change.

It really depends on where you’re living.

Can you make these changes freely because you control everything? Great.

Or are there external consumers you don’t control that are going to depend on this contract for years?

Those are very different situations.

There’s a Cost Either Way

There’s absolutely a cost to this.

You can spend some time upfront figuring out what your API surface looks like and what your responses should look like. You’re spending more time defining what that contract is because that’s exactly what it is: a contract.

Or you can spend that time later.

Maybe later you’re trying to figure out how to version your API. Maybe you’re trying to evolve it without breaking existing clients. Maybe you’re stuck maintaining structures you wish you had designed differently because clients already depend on them.

Either way, you’re likely going to be spending some time somewhere.

How much time you should spend upfront depends entirely on your situation.

If you own all the clients and there are only a few of them, don’t overcomplicate this. If you can change the producer and consumers together, you have options.

If you don’t control the clients, put more thought into the contract.

This Isn’t Just About HTTP Responses

I’m using an HTTP response as the example, but this applies to any type of API or service.

It applies to requests too.

What does a request body actually look like? What does the URI look like?

If you’re not using something like Hypermedia that gives you another way to evolve your HTTP API, you need to put more thought into your URI structures too.

The same applies if you’re building a package or library that people download and use programmatically.

Think about the arguments. Think about the responses. Think about the order of arguments. Think about whether something might throw and how you’re returning the result.

It’s all the same idea.

Put some thought into it from the consumer’s perspective so you’re not unnecessarily creating breaking changes.

Your API Is an Anti-Corruption Layer

To me, this comes down to making a distinction between what’s internal and what’s external.

What’s an implementation detail?

What’s public?

What’s actually part of your contract?

You have clients interacting with your system through an API. That API is the boundary protecting what your actual domain and internals look like.

The way I like to think about it is that your API is an anti-corruption layer.

That’s where you’re doing translation.

This is what the public understands. This is the contract I need to maintain. And I can evolve that separately from how I evolve things internally in my domain.

Let your database change.

Let your domain change.

Keep your contract separate so you can evolve those things independently and do the translation at the boundary.

Another way to say this is: don’t have externals coupled to your internals.

That means don’t expose your data models or domain entities. Don’t do it.

Expose something separately that represents your contract.

You Don’t Need Seven Layers of DTOs

There is a cost and tradeoff to all of this.

That doesn’t mean you need seven different layers of DTO mappings.

You don’t.

What you need to ask is whether you have coupling from something external that you don’t control and can’t change.

If you need to evolve your internals, are those external consumers now screwed? Do they have to follow your breaking changes? Or do you just straight up break them?

If that’s not an option, there’s a cost involved in maintaining your API as an anti-corruption layer. You have to accept that cost because it gives you the ability to evolve your internals separately from your public contract.

But don’t go create mappings and composition just for the sake of doing it.

If you control the clients and there’s a small number of them that you can easily change, maybe you don’t need any of this complexity. You can evolve both sides at the same time.

If you can’t control how consumers evolve, put more thought into the API.

Treat it as a contract.

Keep your internals internal.

And give yourself room to change them.

Join CodeOpinon!
Developer-level members of my Patreon or YouTube channel get access to a private Discord server to chat with other developers about Software Architecture and Design and access to source code for any working demo application I post on my blog or YouTube. Check out my Patreon or YouTube Membership for more info.