Home/Learn/Microservices/API Versioning Strategies

API Versioning Strategies

Intermediate
Communication

Version APIs via URI path (/v1/), query parameter, or Accept header; semantic versioning and backward-compatible changes minimise downstream breakage.

Overview

API versioning is a contract management strategy that allows a service to evolve its API without breaking existing consumers. The three main strategies: **URI path versioning** (`/api/v1/orders`) — most visible and cacheable, widely used in public APIs; **query parameter** (`?version=2`) — less RESTful but easy to test; **Accept header** (content negotiation, `Accept: application/vnd.acme.v2+json`) — purist REST, but harder to test in browsers. The most practical internal microservices approach is **URI versioning** for breaking changes, combined with **backward-compatible evolution** (additive changes only) to avoid versioning for most changes. **Consumer-Driven Contract Testing** (Pact) detects breaking changes before they reach production.

URI Path Versioning

The simplest and most widely adopted approach: embed the version in the URI path. Consumers pin to a specific version. Old versions remain available until all consumers have migrated. The gateway or controller routes to the correct version handler. Use a `@RequestMapping("/api/v{version}/")` base path or separate controllers per version.

Spring Boot — URI path versioning
// Separate controllers per version
@RestController
@RequestMapping("/api/v1/orders")
class OrderControllerV1 {
    @GetMapping("/{id}")
    OrderResponseV1 getOrder(@PathVariable Long id) {
        return orderService.findV1(id);
    }
}

@RestController
@RequestMapping("/api/v2/orders")
class OrderControllerV2 {
    @GetMapping("/{id}")
    OrderResponseV2 getOrder(@PathVariable Long id) {
        return orderService.findV2(id);   // new response shape
    }
}

// Or use a single controller with version routing
@RestController
@RequestMapping("/api/{version}/orders")
class OrderController {
    @GetMapping("/{id}")
    ResponseEntity<?> getOrder(
            @PathVariable String version,
            @PathVariable Long id) {
        return switch (version) {
            case "v1" -> ResponseEntity.ok(orderService.findV1(id));
            case "v2" -> ResponseEntity.ok(orderService.findV2(id));
            default   -> ResponseEntity.notFound().build();
        };
    }
}

Header-Based Versioning and Content Negotiation

Header versioning uses a custom `API-Version` header or `Accept` content-type versioning. It keeps URIs clean and follows HTTP semantics more closely, but is harder to use in browsers and makes caching more complex. Spring MVC supports content-type versioning via `produces`/`consumes` attributes on `@GetMapping`.

Spring Boot — header and media-type versioning
// Custom header versioning
@GetMapping(value = "/api/orders/{id}",
            headers = "X-API-Version=1")
OrderResponseV1 getOrderV1(@PathVariable Long id) {
    return orderService.findV1(id);
}

@GetMapping(value = "/api/orders/{id}",
            headers = "X-API-Version=2")
OrderResponseV2 getOrderV2(@PathVariable Long id) {
    return orderService.findV2(id);
}

// Content-type versioning (vendor MIME type)
@GetMapping(value = "/api/orders/{id}",
            produces = "application/vnd.acme.order.v1+json")
OrderResponseV1 getOrderMediaV1(@PathVariable Long id) { ... }

@GetMapping(value = "/api/orders/{id}",
            produces = "application/vnd.acme.order.v2+json")
OrderResponseV2 getOrderMediaV2(@PathVariable Long id) { ... }

// Consumer request:
// GET /api/orders/123
// Accept: application/vnd.acme.order.v2+json

Backward-Compatible Evolution and Contract Testing

The best versioning strategy is to avoid it: make **additive-only changes** (new optional fields, new endpoints) that don't break existing consumers. **Pact** (Consumer-Driven Contract Testing) verifies that the provider's response still satisfies what each consumer expects — catching breaking changes in CI before they reach production. A breaking change triggers a version bump; additive changes do not.

Microservices — backward-compatible evolution + Pact
// Backward-compatible changes — do NOT require a version bump:
// ✓ Add new optional response field (consumers ignore unknown fields with Jackson)
// ✓ Add a new endpoint
// ✓ Make a required request field optional
// ✓ Add new enum values (if consumers use default handling)

// Breaking changes — REQUIRE a version bump:
// ✗ Remove or rename a field
// ✗ Change a field's type (String → Integer)
// ✗ Make an optional field required
// ✗ Change HTTP status codes for existing responses

// Pact consumer-driven contract test (consumer side)
@ExtendWith(PactConsumerTestExt.class)
class OrderClientPactTest {
    @Pact(provider = "order-service", consumer = "checkout-service")
    RequestResponsePact getOrderPact(PactDslWithProvider builder) {
        return builder
            .given("order 123 exists")
            .uponReceiving("GET /api/v1/orders/123")
            .method("GET").path("/api/v1/orders/123")
            .willRespondWith()
            .status(200)
            .body(new PactDslJsonBody()
                .numberValue("id", 123)
                .stringValue("status", "PAID"))
            .toPact();
    }
}

Key Points to Remember

  • 1URI versioning (/v1/) is most visible, cacheable, and widely used for public APIs
  • 2Header versioning keeps URIs clean but complicates caching and browser testing
  • 3Best strategy: additive-only changes (no version bump) + explicit versioning only for breaking changes
  • 4Breaking changes: remove/rename field, change type, make optional required, change status codes
  • 5Backward-compatible: add optional fields, new endpoints, make required fields optional
  • 6Pact (Consumer-Driven Contract Testing) catches breaking changes in CI before production

Interview Questions

Sign in to ask Aria
1

What are the trade-offs between URI versioning and header-based versioning?

MediumAmazon
2

What changes to an API are considered backward-compatible and which are breaking?

EasyThoughtWorks
3

What is Consumer-Driven Contract Testing and how does Pact work?

HardAtlassian
4

How long should you maintain an old API version after releasing a new one?

MediumStripe
5

How does Jackson's default behaviour help with backward-compatible API evolution?

EasyInfosys

Ask Aria about API Versioning Strategies

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…