Full Stack • Java • System Design • Cloud • AI Engineering

API Versioning

Learn API Versioning from scratch with REST API evolution strategies, real-world enterprise examples, architecture diagrams, Spring Boot implementation, and API lifecycle management.

Introduction

Imagine you are a mobile app developer at a bank.

Your app connects to the bank's API:

Mobile App v1.0
     ↓
GET /api/accounts
     ↓
{ "accountNumber": "1234", "balance": 5000 }

Everything works perfectly.

Six months later, the backend team makes changes to support new features.

They rename balance to availableBalance and add new required fields.

Now:

Mobile App v1.0
     ↓
GET /api/accounts
     ↓
{ "accountNumber": "1234", "availableBalance": 5000, "currency": "USD" }

Your old mobile app breaks.

Thousands of customers cannot check their account balance.

The question is:

How do you evolve your API without breaking existing clients?

This is exactly the problem that API Versioning solves.


Learning Objectives

After completing this article, you will understand:

  • What is API Versioning?
  • Why APIs Need Versioning
  • API Lifecycle
  • Types of API Versioning
  • Comparison of Versioning Strategies
  • Banking and E-Commerce Use Cases
  • Request Flow with API Gateway
  • Spring Boot Implementation
  • DTO Evolution
  • Database Change Strategies
  • Backward Compatibility Rules
  • Deprecation Strategy
  • Microservices Perspective
  • Best Practices and Common Mistakes

What is API Versioning?

API Versioning is the practice of managing changes to an API while ensuring that existing clients continue to work without modification.

When you release a new version of your API:

  • Old clients continue using the previous version
  • New clients adopt the new version
  • Both versions coexist during a migration period
Mobile App v1.0  →  API v1  →  Works
Mobile App v2.0  →  API v2  →  Works
Partner System   →  API v1  →  Still Works

Why APIs Need Versioning

APIs change for many reasons:

  • Adding new fields to responses
  • Removing or renaming existing fields
  • Changing authentication mechanisms
  • Introducing new endpoints
  • Changing request/response formats
  • Performance improvements that alter behavior

The fundamental problem is:

You cannot control when your clients will upgrade.

A mobile app update requires the user to manually update from the App Store or Play Store.

A partner system integration may take months to update.

Enterprise clients may be locked into specific API versions for compliance reasons.


What Happens Without Versioning

Consider a real-world Banking example.

Before Change

GET /api/transfer

Request:
{
  "fromAccount": "1234",
  "toAccount": "5678",
  "amount": 1000
}

Response:
{
  "status": "SUCCESS",
  "transferId": "TXN001"
}

After Change (Breaking)

The backend team adds mandatory fraud detection fields:

GET /api/transfer

Request:
{
  "fromAccount": "1234",
  "toAccount": "5678",
  "amount": 1000,
  "reason": "RENT",           ← New required field
  "deviceId": "DEVICE-XYZ"    ← New required field
}

Old mobile apps do not send reason and deviceId.

Result:

Old Mobile Apps
     ↓
Transfer API
     ↓
HTTP 400 Bad Request
     ↓
Transfer Fails
     ↓
Customer Complaints
     ↓
Revenue Loss

API Lifecycle

Every API goes through a defined lifecycle:

flowchart LR
    A[Design] --> B[Development]
    B --> C[Testing]
    C --> D[Release v1]
    D --> E[Stable v1]
    E --> F[Release v2]
    F --> G[v1 Deprecated]
    G --> H[v1 Retired]
    F --> I[Stable v2]
Phase Description
Design API contract designed, documented
Development API implemented and tested internally
Release API published and available to clients
Stable API in active use, no breaking changes
Deprecated A newer version exists, old version still works
Retired Old version shut down, clients must have migrated

Types of API Versioning

There are five main strategies for versioning APIs.

1. URI Versioning

The version number is embedded directly in the URL path.

GET /api/v1/accounts
GET /api/v2/accounts
GET /api/v3/accounts

This is the most widely used strategy and is easy to understand and test.


2. Query Parameter Versioning

The version is passed as a query parameter.

GET /api/accounts?version=1
GET /api/accounts?version=2

Simple to implement, but can be accidentally omitted.


3. Header Versioning

The version is passed in a custom HTTP request header.

GET /api/accounts
Accept-Version: v1

GET /api/accounts
Accept-Version: v2

Keeps URLs clean but is harder to test in a browser.


4. Media Type Versioning (Content Negotiation)

The version is embedded in the Accept header as a custom media type.

GET /api/accounts
Accept: application/vnd.bank.v1+json

GET /api/accounts
Accept: application/vnd.bank.v2+json

Very flexible and RESTful but complex to implement and document.


5. Hostname Versioning

Different API versions are served from different subdomains.

https://v1.api.bank.com/accounts
https://v2.api.bank.com/accounts

Allows complete infrastructure isolation per version, but increases operational overhead.


Versioning Strategy Comparison

Strategy Pros Cons Recommended For
URI Versioning Simple, visible, easy to test URL bloat over time Most public REST APIs
Query Parameter Simple to implement Easy to miss, caching complications Internal APIs, quick rollouts
Header Versioning Clean URLs Hard to test in browser, less visible Enterprise B2B APIs
Media Type Most RESTful, flexible Complex to implement and document Hypermedia-driven APIs
Hostname Full infrastructure isolation DNS management, TLS certs per version Large-scale platform APIs

Industry recommendation: URI Versioning is preferred for most public APIs due to its visibility, simplicity, and developer experience.


Banking Use Case

A bank has three types of clients:

  • Legacy mobile app (iOS v1.0)
  • New mobile app (iOS v3.0)
  • Partner fintech systems
flowchart TD
    A[Legacy Mobile App v1.0]
    B[New Mobile App v3.0]
    C[Partner Fintech System]

    D[API Gateway]

    E[Account Service v1]
    F[Account Service v2]

    G[(Database)]

    A -->|/api/v1/accounts| D
    B -->|/api/v2/accounts| D
    C -->|/api/v1/accounts| D

    D --> E
    D --> F

    E --> G
    F --> G

Both v1 and v2 connect to the same database.

The difference is in the response shape:

v1 Response (Legacy)

{
  "accountNumber": "ACC-1234",
  "balance": 5000
}

v2 Response (New)

{
  "accountNumber": "ACC-1234",
  "availableBalance": 5000,
  "currency": "INR",
  "lastUpdated": "2026-07-01T10:00:00Z"
}

Old clients get what they expect. New clients get richer data.


Request Flow Through API Gateway

flowchart TD
    A[Client Request]
    B[API Gateway]
    C{Version Detection}

    D[v1 Controller]
    E[v2 Controller]

    F[Business Service Layer]

    G[(Database)]

    A --> B
    B --> C

    C -->|v1| D
    C -->|v2| E

    D --> F
    E --> F

    F --> G

The API Gateway:

  1. Receives the incoming request
  2. Inspects the URL, header, or query parameter for version info
  3. Routes the request to the correct version controller
  4. The business service layer and database remain shared

Architecture: Multiple Clients and Versions

flowchart TD
    subgraph Clients
        A[Mobile App v1]
        B[Mobile App v2]
        C[Web App]
        D[Partner API]
    end

    E[API Gateway]

    subgraph Version Router
        F[API v1]
        G[API v2]
        H[API v3]
    end

    I[Business Services]

    A --> E
    B --> E
    C --> E
    D --> E

    E --> F
    E --> G
    E --> H

    F --> I
    G --> I
    H --> I

Key insight: The Version Router isolates interface contracts. The Business Services remain stable and shared.


Spring Boot Implementation

Spring Boot supports API versioning natively through request mapping annotations.

URI Versioning

// Version 1
@RestController
@RequestMapping("/api/v1/accounts")
public class AccountControllerV1 {

    @GetMapping("/{id}")
    public AccountResponseV1 getAccount(@PathVariable String id) {
        return accountService.getAccountV1(id);
    }
}

// Version 2
@RestController
@RequestMapping("/api/v2/accounts")
public class AccountControllerV2 {

    @GetMapping("/{id}")
    public AccountResponseV2 getAccount(@PathVariable String id) {
        return accountService.getAccountV2(id);
    }
}

Header Versioning

@RestController
@RequestMapping("/api/accounts")
public class AccountController {

    @GetMapping(value = "/{id}", headers = "Accept-Version=v1")
    public AccountResponseV1 getAccountV1(@PathVariable String id) {
        return accountService.getAccountV1(id);
    }

    @GetMapping(value = "/{id}", headers = "Accept-Version=v2")
    public AccountResponseV2 getAccountV2(@PathVariable String id) {
        return accountService.getAccountV2(id);
    }
}

Query Parameter Versioning

@RestController
@RequestMapping("/api/accounts")
public class AccountController {

    @GetMapping(value = "/{id}", params = "version=1")
    public AccountResponseV1 getAccountV1(@PathVariable String id) {
        return accountService.getAccountV1(id);
    }

    @GetMapping(value = "/{id}", params = "version=2")
    public AccountResponseV2 getAccountV2(@PathVariable String id) {
        return accountService.getAccountV2(id);
    }
}

Media Type (Content Negotiation) Versioning

@RestController
@RequestMapping("/api/accounts")
public class AccountController {

    @GetMapping(
        value = "/{id}",
        produces = "application/vnd.bank.v1+json"
    )
    public AccountResponseV1 getAccountV1(@PathVariable String id) {
        return accountService.getAccountV1(id);
    }

    @GetMapping(
        value = "/{id}",
        produces = "application/vnd.bank.v2+json"
    )
    public AccountResponseV2 getAccountV2(@PathVariable String id) {
        return accountService.getAccountV2(id);
    }
}

DTO Evolution

DTOs (Data Transfer Objects) must evolve carefully to avoid breaking clients.

v1 DTO

public class AccountResponseV1 {
    private String accountNumber;
    private Double balance;
}

v2 DTO (Evolved)

public class AccountResponseV2 {
    private String accountNumber;
    private Double availableBalance;   // renamed
    private String currency;           // new field
    private LocalDateTime lastUpdated; // new field
}

Mapper

@Service
public class AccountMapper {

    public AccountResponseV1 toV1(Account account) {
        return AccountResponseV1.builder()
            .accountNumber(account.getAccountNumber())
            .balance(account.getAvailableBalance())
            .build();
    }

    public AccountResponseV2 toV2(Account account) {
        return AccountResponseV2.builder()
            .accountNumber(account.getAccountNumber())
            .availableBalance(account.getAvailableBalance())
            .currency(account.getCurrency())
            .lastUpdated(account.getLastUpdated())
            .build();
    }
}

The domain object (Account) is shared. Only the response shape differs per version.


Database Change Strategies

Changing the database schema is one of the most risky aspects of API versioning.

Safe: Adding a New Column

-- Safe: existing queries are unaffected
ALTER TABLE accounts ADD COLUMN currency VARCHAR(3) DEFAULT 'INR';

All existing queries still work. New queries can use the new column.


Safe: Making a Column Nullable

-- Safe: existing rows are unaffected
ALTER TABLE accounts MODIFY COLUMN phone_number VARCHAR(20) NULL;

Risky: Renaming a Column

Instead of directly renaming, use a migration strategy:

Step 1: Add new column
ALTER TABLE accounts ADD COLUMN available_balance DECIMAL(18,2);

Step 2: Migrate data
UPDATE accounts SET available_balance = balance;

Step 3: Update application to write to both columns

Step 4: After all clients migrate to v2, drop the old column
ALTER TABLE accounts DROP COLUMN balance;

Risky: Removing a Column

Step 1: Mark field as deprecated in v1 response (return null or default)
Step 2: Release v2 without the field
Step 3: Communicate sunset date to all clients
Step 4: After retirement, drop the column

Never remove a column that active API versions still depend on.


API Gateway Integration

API Gateways handle version routing before requests reach your application.

flowchart TD
    A[Client]
    B[API Gateway]

    C{Route by Version}

    D[v1 Service — Port 8081]
    E[v2 Service — Port 8082]

    A --> B
    B --> C
    C -->|/api/v1/| D
    C -->|/api/v2/| E

Kong Gateway Configuration

services:
  - name: account-service-v1
    url: http://account-service:8081
    routes:
      - name: v1-route
        paths:
          - /api/v1

  - name: account-service-v2
    url: http://account-service:8082
    routes:
      - name: v2-route
        paths:
          - /api/v2

Spring Cloud Gateway Configuration

spring:
  cloud:
    gateway:
      routes:
        - id: api-v1
          uri: http://account-service-v1:8081
          predicates:
            - Path=/api/v1/**

        - id: api-v2
          uri: http://account-service-v2:8082
          predicates:
            - Path=/api/v2/**

AWS API Gateway

AWS API Gateway natively supports stage-based versioning:

https://abc123.execute-api.us-east-1.amazonaws.com/v1/accounts
https://abc123.execute-api.us-east-1.amazonaws.com/v2/accounts

Each stage can point to a different Lambda function or backend service.


Version Negotiation Flow

sequenceDiagram
    participant Client
    participant Gateway
    participant VersionRouter
    participant Service

    Client->>Gateway: GET /api/v2/accounts/1234
    Gateway->>VersionRouter: Detect version from path
    VersionRouter->>Service: Route to v2 handler
    Service->>VersionRouter: AccountResponseV2
    VersionRouter->>Gateway: Response
    Gateway->>Client: HTTP 200 with v2 response

Backward Compatibility Rules

Backward compatibility means old clients continue working without any changes when you update your API.

Rules

Never do:

  • Remove a required request field
  • Change the data type of an existing field
  • Rename an existing field in the response
  • Change the meaning of an existing status code

Always safe:

  • Add new optional request fields
  • Add new response fields
  • Add new endpoints
  • Add new query parameters with default values
v1 Response:
{
  "accountNumber": "1234",
  "balance": 5000
}

v2 Response (Backward Compatible Addition):
{
  "accountNumber": "1234",
  "balance": 5000,          ← Still present for old clients
  "currency": "INR",        ← New field, ignored by old clients
  "lastUpdated": "2026-07-01"
}

Old clients read balance as expected. New clients can additionally read currency and lastUpdated.


Deprecation Strategy

Deprecating a version requires clear communication and a migration window.

flowchart LR
    A[v1 Active] --> B[v2 Released]
    B --> C[v1 Deprecated]
    C --> D[Warning Headers Added to v1]
    D --> E[Migration Guide Published]
    E --> F[6-Month Migration Window]
    F --> G[v1 Retired]

Deprecation Warning Header

When a client calls a deprecated version, return a warning header:

HTTP/1.1 200 OK
Deprecation: true
Sunset: Sat, 01 Jan 2027 00:00:00 GMT
Link: <https://api.bank.com/docs/migration/v1-to-v2>; rel="successor-version"

Deprecation Timeline Best Practices

Step Recommended Timeline
Release v2 Day 0
Announce v1 deprecation Day 0
Add deprecation headers Day 0
Send migration guide Week 1
Grace period 6–12 months for public APIs
Final retirement notice 90 days before sunset
Retire v1 End of grace period

Enterprise Banking Example

A bank evolves its Transfer API across three versions to support new payment methods.

flowchart LR
    A[v1: Basic Transfer]
    B[v2: Transfer + QR Code]
    C[v3: Transfer + QR + International]

    A --> B
    B --> C

v1 — Basic Transfer

POST /api/v1/transfer
{
  "fromAccount": "1234",
  "toAccount": "5678",
  "amount": 1000
}

v2 — QR Code Support Added

POST /api/v2/transfer
{
  "fromAccount": "1234",
  "toAccount": "5678",
  "amount": 1000,
  "qrCode": "QR_XYZ_123"
}

v3 — International Transfer

POST /api/v3/transfer
{
  "fromAccount": "1234",
  "toAccount": "5678",
  "amount": 1000,
  "currency": "USD",
  "swiftCode": "BANKUS33"
}

Old mobile apps continue using v1 without any changes.

New apps use v2 or v3 to access new features.


E-Commerce Example

Amazon-style checkout evolves over time without breaking existing integrations.

v1 Checkout

POST /api/v1/checkout
{
  "cartId": "CART-001",
  "address": { "line1": "123 Main St", "city": "New York" },
  "payment": { "cardNumber": "4111111111111111" }
}

v2 Checkout (Extended)

POST /api/v2/checkout
{
  "cartId": "CART-001",
  "address": { "line1": "123 Main St", "city": "New York" },
  "payment": { "cardNumber": "4111111111111111" },
  "couponCode": "SAVE20",
  "giftCard": "GIFT-500",
  "loyaltyPoints": 200
}

v1 integrations from partner websites continue working.

v2 users get access to promotions, gift cards, and loyalty points.


Microservices Perspective

In a microservices architecture, each service manages its own API version independently.

flowchart TD
    A[API Gateway]

    B[Account Service v1/v2]
    C[Payment Service v1/v3]
    D[Notification Service v1/v2]

    A --> B
    A --> C
    A --> D

Key principles:

  • Each microservice owns its versioning lifecycle
  • Services can be on different versions simultaneously
  • Teams deploy independently without coordination
  • The API Gateway handles routing and version mapping

Best Practices

  • Prefer backward-compatible changes — add fields rather than remove or rename them
  • Use URI versioning for public APIs — it is the most widely understood strategy
  • Version only when necessary — avoid creating a new version for every small change
  • Document all versions — maintain up-to-date documentation for every active version
  • Communicate deprecation early — give clients at least 6 months to migrate
  • Add deprecation headers — programmatically warn clients that a version is sunset
  • Monitor version usage — track which clients use which versions before retiring
  • Set a migration grace period — never retire a version without a defined sunset date
  • Use semantic versioningv1, v2, v3 for major breaking changes only
  • Keep the number of active versions small — more versions mean more maintenance cost

Common Mistakes

  • Breaking existing clients without a migration window — causes outages for mobile apps and partner systems
  • Versioning too frequently — every bug fix does not need a new version
  • Versioning internal APIs unnecessarily — internal services under your control do not always need strict versioning
  • Maintaining deprecated versions indefinitely — creates technical debt and security risks
  • Mixing multiple versioning strategies — using URI versioning in some APIs and header versioning in others creates confusion for developers
  • Not monitoring version adoption — you cannot retire a version without knowing who still uses it

Summary

Topic Key Takeaway
What is API Versioning Managing API changes without breaking existing clients
Why it matters Clients cannot upgrade instantly; backward compatibility is critical
URI Versioning Most widely used; version in URL path (/api/v1/)
Header Versioning Clean URLs; version in Accept-Version header
Media Type Versioning Most RESTful; version in Accept media type
DTO Evolution Map same domain object to different response shapes per version
Database Changes Add columns safely; never remove columns used by active versions
Backward Compatibility Add optional fields; never remove or rename existing fields
Deprecation Warn via headers; provide migration guide; respect grace period
Spring Boot Use @RequestMapping, headers, or params for version routing
Microservices Each service versions independently; gateway routes by version
Best Practice Minimize active versions; communicate early; monitor adoption

API Versioning is a critical skill for any engineer building APIs that are consumed by external or mobile clients. Applying the right versioning strategy from the start saves enormous migration pain later.