Backwards Compatibility Is a Promise. Stop Breaking It.

by Arif Ikhsanudin, Backend Developer

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

Why Documentation Is a Leadership Skill, Not a Chore

Writing good documentation is not about being thorough or disciplined. It's about understanding what the people who come after you will actually need — and caring enough to provide it.

Read more

Stop Creating Branches You Never Clean Up

Stale branches are not just clutter — they create confusion about what is active work, slow down tab-completion, and make it harder to understand the state of the repository at a glance.

Read more

Tools That Actually Save You Hours as a Junior Contractor

As a junior contractor, every minute counts. The right tools don’t just make you faster—they save your sanity. Here’s a guide to tools that genuinely cut down the chaos.

Read more

Lockheed, Boeing and Raytheon Set Denver's Backend Salary Bar — Startups Cannot Clear It

Denver's defense and aerospace industries pay for backend engineering talent at a scale most startups can't match. The hiring market reflects it.

Read more