API Versioning in Microservices Is Not Optional

by Eric Hanson, Backend Developer at Clean Systems Consulting

The incident that makes teams take this seriously

The Order Service adds a new field to the user response payload — preferredCurrency, returned as a string. Three other services consume this endpoint. One of them, the Reporting Service, was written by a different team that used a strict JSON deserializer that fails on unknown fields. The Reporting Service starts crashing. Nobody realizes why until thirty minutes into the incident, because the Reporting Service logs aren't obviously connected to the Order Service deployment.

This is not an unusual scenario. It's the canonical API compatibility incident in microservices, and it plays out in some variation at almost every organization that doesn't have a versioning strategy from the start.

What "breaking change" actually means

A breaking change is any modification to an API contract that causes existing consumers to behave incorrectly. The list is longer than most teams assume:

  • Removing a field from a response
  • Renaming a field
  • Changing a field's type (string to integer, nullable to non-null)
  • Changing an enum's values
  • Adding a required field to a request
  • Changing the semantics of an existing field (changing what a status value means)
  • Changing error response shapes
  • Changing HTTP status codes

Non-breaking changes are additive: adding optional fields to responses, adding optional fields to requests (with documented defaults), adding new endpoints.

The problem is that "non-breaking" depends on how consumers are written. A consumer using a strict deserializer that rejects unknown fields will break on a response that adds a new field. A consumer that pattern-matches on the full status string will break if you add new status values. API versioning is about making your stability guarantees explicit and enforced, not about eliminating all risk.

Versioning strategies and their tradeoffs

URL path versioning (/v1/orders, /v2/orders) is the most visible and most commonly understood approach. It's explicit: consumers know exactly which version they're on, they control when they upgrade, and multiple versions can coexist in the same deployment.

The downside is duplication. Running /v1 and /v2 simultaneously means maintaining logic for both. If the difference is substantial, you're maintaining two codebases. The usual mitigation is to have versioned controllers that delegate to shared domain logic with version-appropriate request/response mapping.

Header versioning (Accept: application/vnd.myapp.v2+json or API-Version: 2) keeps URLs clean and is more REST-correct, but it's harder for consumers to discover and harder to test in a browser. Teams that have strong API clients (generated from OpenAPI specs) handle it fine. Teams with ad-hoc HTTP calls find it error-prone.

Consumer-driven contract testing (Pact) is not a versioning strategy but it is the best enforcement mechanism for any versioning strategy. Consumer teams publish contracts defining what they expect from each provider endpoint. The provider's CI runs those contracts as tests:

// Pact consumer test (Order Service expecting from User Service)
@Pact(consumer = "OrderService", provider = "UserService")
RequestResponsePact createPact(PactDslWithProvider builder) {
    return builder
        .given("user 123 exists")
        .uponReceiving("a request for user order context")
        .path("/users/123/order-context")
        .method("GET")
        .willRespondWith()
        .status(200)
        .body(new PactDslJsonBody()
            .stringType("userId", "123")
            .booleanType("isEligible", true)
            .decimalType("creditLimit", 1000.00))
        .toPact();
}

If User Service removes creditLimit, the Pact test fails in User Service's CI before the change is merged. The breaking change is caught before deployment, not during an incident.

Deprecation as a process, not an event

Versioning gives you the mechanism for backward compatibility. Deprecation gives you the process for retiring old versions without surprises. The minimum viable deprecation process:

  1. Add a Deprecation header to responses from the old version with the deprecation date (RFC 8594)
  2. Log or metric-tag requests hitting deprecated endpoints so you know which consumers are still using them
  3. Reach out to identified consumers with a migration timeline
  4. Enforce a minimum deprecation window (six months is standard; three is aggressive)
HTTP/1.1 200 OK
Deprecation: Sat, 01 Jan 2027 00:00:00 GMT
Sunset: Sat, 01 Jan 2027 00:00:00 GMT
Link: <https://api.example.com/v2/orders>; rel="successor-version"

Without request logging per version, you'll never know if any consumer is still using the old version. You'll sunset it and someone will file an urgent incident ticket.

The practical minimum

If you're starting a new service today: version from /v1 immediately, even if you think you won't need it. Adding versioning retroactively to a service with multiple consumers is significantly more painful than starting with it. Use URL path versioning if your API is consumed by external parties or multiple internal teams who deploy independently. Use contract testing in CI to catch breaking changes before they deploy.

If you're operating an unversioned service with multiple consumers: start by instrumenting which consumers call which endpoints. Add the Deprecation header to any endpoint you plan to change. Before any schema change, verify with Pact or equivalent whether it's breaking for current consumers. Add versioning to new endpoints going forward and accept the hybrid state as a transitional cost.

The investment in versioning discipline pays for itself the first time it prevents an incident. It pays for itself several times over the first time it lets two teams deploy independently without a coordination meeting.

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 to Evaluate a Backend Project Before Accepting the Work

“Seems doable… but something feels off.” That hesitation is worth listening to — especially in backend work.

Read more

Why MVC Is Not Enough for Complex Backend Systems

MVC is great for small apps, but when your backend starts juggling caching, queues, and multiple APIs, it quickly shows its limits.

Read more

Deadlocks in Java — How They Form, How to Find Them, and How to Design Around Them

Deadlocks are deterministic — given the same lock acquisition order and timing, they reproduce reliably. Understanding the four conditions that create them makes both prevention and diagnosis systematic rather than guesswork.

Read more

Setting Career Goals as a Contractor

No promotion cycle. No manager checking your progress. As a contractor, your career only moves if you decide where it’s going.

Read more