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:
- Receives the incoming request
- Inspects the URL, header, or query parameter for version info
- Routes the request to the correct version controller
- 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 versioning —
v1,v2,v3for 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.