Backwards Compatibility Is a Promise. Stop Breaking It.

by Eric Hanson, Backend Developer at Clean Systems Consulting

What backwards compatibility actually means

It means that code written against your API today will continue to work correctly after you make changes — without any modifications on the client's part. Not mostly work. Not work except for this one edge case. Work.

This sounds strict because it is. The promise is binary. You either maintained it or you did not.

The counterargument is that strict backwards compatibility is too constraining — it prevents you from fixing mistakes, improving your schema, and evolving the API. That is true. The resolution is not to break compatibility silently. It is to use versioning to make breaking changes on a separate version while maintaining the old contract on the existing one.

Breaking backwards compatibility without a version bump is not evolution. It is unilateral breakage passed off as normal maintenance.

The forms it takes

The obvious violations are easy to identify: removing fields, renaming endpoints, changing authentication requirements. Most teams know better than to do these without a version bump.

The subtle violations are where the real damage happens.

Changing response field types silently: A field that returned integers starts returning floats because the underlying calculation changed. In statically typed client languages, this throws a deserialization exception. The client worked yesterday. Nothing in your changelog mentioned this.

Adding server-side validation to previously accepted values: Your API accepted any string in the currency field. You add ISO 4217 validation. Now clients passing "USD " (with a trailing space) get 400 errors on previously valid requests.

Changing sort order without documentation: A paginated endpoint returned results ordered by creation time, ascending. A performance optimization changed the default to id order, which is almost always the same but diverges for bulk-imported data. Clients doing cursor-based pagination now miss records.

Changing error codes: A client handles 402 Payment Required for overdue accounts. You change it to 403 Forbidden because it is technically more correct. The client's error handling no longer catches subscription failures.

None of these feel like breaking changes when you are writing them. They all are.

Enforcement is the only thing that works

Policy documents do not prevent breaking changes. Individual discipline does not prevent breaking changes. The only reliable prevention is automated enforcement.

OpenAPI schema diffing in CI catches structural changes — field removals, type changes, required field additions. Tools like oasdiff can be configured to fail a PR that introduces breaking changes without an explicit version bump:

# .github/workflows/api-compat.yml
- name: Check API backwards compatibility
  run: |
    oasdiff breaking openapi-main.yaml openapi-pr.yaml \
      --fail-on ERR \
      --format text

This does not catch behavioral changes — sort order, validation rules, status code semantics. Those require contract tests.

Contract tests run the actual client expectations against the current API:

// A contract the client registered
describe('GET /v1/users/:id', () => {
  it('returns user with string id field', async () => {
    const res = await apiClient.get('/v1/users/42');
    expect(typeof res.body.id).toBe('string');
    expect(res.status).toBe(200);
  });

  it('returns 404 for unknown user, not 400', async () => {
    const res = await apiClient.get('/v1/users/99999999');
    expect(res.status).toBe(404);
  });
});

These tests encode the implicit contract — not just the happy path but the edge case behaviors clients depend on. Run them in CI against every server-side change.

When you have to break something

There are cases where backwards compatibility genuinely cannot be maintained — a security vulnerability requires invalidating all tokens, a fundamental data model change has no backwards-compatible path.

In these cases:

  1. Make the break explicit. Bump the version. Do not slip it into a patch release and hope nobody notices.
  2. Communicate the change with a concrete migration timeline before you ship.
  3. Provide a compatibility shim if at all possible during a migration window.
  4. For security-driven breaks, explain the reason clearly. Developers accept necessary breakage they understand; they resent arbitrary breakage they do not.

The bar for "genuinely cannot be maintained" should be high. In most cases, there is a backwards-compatible path — it just takes more engineering effort than the non-compatible path. That effort is the cost of having made a contract with your users.

The organizational question

Breaking backwards compatibility is often not a technical decision — it is a prioritization decision. The engineering work to maintain compatibility gets weighed against feature velocity and the path of least resistance wins.

The solution is to make backwards compatibility breakage visible and costly in the process. Require explicit sign-off from an API owner before any breaking change ships. Count public breaking changes as incidents. Track them. When the cost of breaking compatibility is visible, it gets weighed correctly.

Scale Your Backend - Need an Experienced Backend Developer?

We provide backend engineers who join your team as contractors to help build, improve, and scale your backend systems.

We focus on clean backend design, clear documentation, and systems that remain reliable as products grow. Our goal is to strengthen your team and deliver backend systems that are easy to operate and maintain.

We work from our own development environments and support teams across US, EU, and APAC timezones. Our workflow emphasizes documentation and asynchronous collaboration to keep development efficient and focused.

  • Production Backend Experience. Experience building and maintaining backend systems, APIs, and databases used in production.
  • Scalable Architecture. Design backend systems that stay reliable as your product and traffic grow.
  • Contractor Friendly. Flexible engagement for short projects, long-term support, or extra help during releases.
  • Focus on Backend Reliability. Improve API performance, database stability, and overall backend reliability.
  • Documentation-Driven Development. Development guided by clear documentation so teams stay aligned and work efficiently.
  • Domain-Driven Design. Design backend systems around real business processes and product needs.

Tell us about your project

Our offices

  • Copenhagen
    1 Carlsberg Gate
    1260, København, Denmark
  • Magelang
    12 Jalan Bligo
    56485, Magelang, Indonesia

More articles

How “Simple Tasks” Always Take Longer Than Expected

You plan for a quick fix, a 10-minute tweak, or a small update—but suddenly hours vanish. Why do the “simple” tasks end up consuming more time than your complex ones?

Read more

Service Objects in Ruby — How I Structure Business Logic

Service objects are the most argued-about pattern in Rails codebases and the least defined. Here is a concrete structure that handles initialization, result signaling, and error propagation without pulling in a framework.

Read more

Wellington's Government Sector Hires the Backend Developers That Startups Need

Wellington produces capable backend engineers. The public sector finds them first and gives them reasons to stay.

Read more

Microservices Sound Great Until You Have to Maintain Them

Microservices trade one class of problem for several others. The architecture is legitimate — but teams routinely adopt it before they have the operational maturity to survive it.

Read more