API Versioning in Microservices Is Not Optional

by Arif Ikhsanudin, Backend Developer

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

Stop Designing APIs for Yourself. Design Them for the Person Calling Them.

APIs often reflect how the backend is built instead of how they are used. Shifting the perspective to the consumer leads to simpler integrations, fewer errors, and more durable systems.

Read more

What Integration Tests Should Actually Be Testing

Integration tests are expensive to write and run. Spending that cost on the wrong things — logic that belongs in unit tests, workflows that belong in end-to-end tests — wastes the investment. Here is how to focus integration test effort where it creates the most value.

Read more

Spring Data JPA Auditing — @CreatedDate, @LastModifiedBy, and Entity Lifecycle Tracking

Audit fields — who created this record and when, who last modified it — are required in most production applications. Spring Data JPA provides this automatically with minimal configuration, but the integration with bulk operations, tests, and security context has specific traps worth knowing.

Read more

Warsaw Backend Developer Costs Are Rising Faster Than Most Startups Expected — Here Is the Alternative

The salary arbitrage that made Warsaw attractive for backend hiring has been compressing for years. Most startup budgets haven't caught up to the new reality.

Read more