Skip to main content

API Contracts and Backward Compatibility

Design stable API contracts and manage schema evolution to support multiple client versions without breaking deployments or forcing synchronized updates.

TL;DR

Backward compatibility ensures old clients work with new API versions without changes. Practice semantic versioning (MAJOR.MINOR.PATCH): bump MAJOR for breaking changes, MINOR for non-breaking additions, PATCH for fixes. Add optional fields and ignore unknown fields (robustness principle). Deprecate gradually with multi-version support before removal. Use schema registries to track changes; employ contract testing to detect breaking changes in CI. Support multiple API versions (v1, v2) in parallel for migration windows rather than hard-cutoff deployments.

Learning Objectives

By the end of this article, you'll understand:

  • Backward vs forward compatibility and safe change patterns
  • Semantic versioning conventions and application
  • Breaking change detection and prevention strategies
  • Deprecation policies and migration windows
  • Schema evolution techniques and compatibility layers
  • Contract testing for API stability

Motivating Scenario

You release API v2 with a cleaner schema, dropping deprecated fields and reorganizing response structure. Hundreds of mobile apps still use v1 endpoints; updating all of them requires coordinating with users and waiting for app store approvals. Deployment breaks during rollout, you can't roll back because the new database schema isn't compatible. You need a backward compatibility strategy allowing v1 clients to work indefinitely while supporting v2 for new clients, enabling gradual migration without forced updates.

Core Concepts

Backward Compatibility Patterns

Additive Changes (Safe):

  • Add optional fields (clients ignoring unknown fields)
  • Add new endpoints (old endpoints still work)
  • Add new error codes (clients ignore new ones)
  • Change internal implementation

Breaking Changes (Requires Version Bump):

  • Remove fields or endpoints
  • Rename fields
  • Change field types or formats
  • Change status codes for same scenario
  • Change HTTP method

Semantic Versioning

MAJOR.MINOR.PATCH format:

  • MAJOR: Breaking changes (v1 → v2)
  • MINOR: Non-breaking additions (v1.2 → v1.3)
  • PATCH: Bug fixes (v1.2.0 → v1.2.1)

Example: v2.3.5

  • v2: Breaking changes since v1
  • v3: New breaking changes since v2.x
  • .5: Fifth patch level

Schema Evolution Techniques

Strict Mode (Fail on Unknown Fields):

// Old schema
{ "name": "string", "age": "number" }

// New response with backward compat
{ "name": "string", "age": "number", "email": "string" }

// Client expecting old schema
// If client fails on unknown "email", incompatible

Lenient Mode (Ignore Unknown Fields): Most modern clients ignore unknown fields by design. Safe to add new fields without version bump.

Schema Versioning: Include schema version in payload for independent schema vs API versioning.

Practical Example

# Order Service API - Multiple versions in one spec

openapi: 3.1.0
info:
title: Order Service API
version: 2.0.0
x-api-lifecycle:
deprecated_versions:
- "1.0.0"
deprecation_date: "2024-01-01"
sunset_date: "2024-07-01"
migration_guide: "https://docs.example.com/migration"

servers:
- url: https://api.example.com/v2
description: Current API (recommended)
- url: https://api.example.com/v1
description: Deprecated API (sunset 2024-07-01)

paths:
/orders:
post:
summary: Create order
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/CreateOrderRequest'
responses:
'201':
description: Order created
content:
application/json:
schema:
$ref: '#/components/schemas/Order'

components:
schemas:
CreateOrderRequest:
type: object
required:
- items
- shipping_address
properties:
items:
type: array
items:
$ref: '#/components/schemas/OrderItem'
minItems: 1
shipping_address:
$ref: '#/components/schemas/Address'
# NEW in v2 - optional for clients not yet upgraded
billing_address:
$ref: '#/components/schemas/Address'
nullable: true
# NEW in v2 - optional for backward compat
metadata:
type: object
additionalProperties: true
nullable: true

Order:
type: object
required:
- id
- status
- total
- created_at
properties:
id:
type: string
format: uuid
status:
type: string
enum: [PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED]
# NEW in v2.1: added CANCELLED
total:
type: number
format: decimal
created_at:
type: string
format: date-time
# NEW in v2: optional field for new clients
# Old clients ignore, new clients use
estimated_delivery:
type: string
format: date
nullable: true
# NEW in v2: nested address object
# Replaces old "shipping_address_str"
shipping_address:
$ref: '#/components/schemas/Address'

# Deprecated in v2 (moved to Address object)
# Kept for v1 API backward compatibility
x-deprecated-fields:
shipping_address_str:
deprecated: true
removed_in: "3.0"
use_instead: "shipping_address"

Address:
type: object
required:
- street
- city
- country
properties:
street: { type: string }
city: { type: string }
state: { type: string, nullable: true }
postal_code: { type: string }
country: { type: string }

OrderItem:
type: object
required:
- product_id
- quantity
properties:
product_id: { type: string }
quantity: { type: integer, minimum: 1 }
unit_price: { type: number, format: decimal, nullable: true }

headers:
X-API-Version:
description: API version (e.g., 2.0, 2.1)
schema: { type: string }
X-Deprecation:
description: "Deprecation warning with sunset date"
schema: { type: string }
example: "API v1 is deprecated, sunset 2024-07-01"

responses:
DeprecatedVersion:
description: API version is deprecated
headers:
X-Deprecation:
description: Deprecation notice
schema: { type: string }
content:
application/json:
schema:
type: object
properties:
warning: { type: string }
sunset_date: { type: string, format: date }
migration_guide: { type: string, format: uri }

When to Use / When Not to Use

Support Multiple Versions When:
  1. Large mobile user base (can
  2. ,
  3. t control deployment)
  4. Multiple deployment pipelines (rolling releases)
  5. Long support window required (>1 year)
  6. Schema changes complex (nested structures)
Hard-Cut Migration When:
  1. Single-server deployment (all clients update together)
  2. Internal APIs only (full control of consumers)
  3. API still in beta/experimental phase
  4. Complete redesign necessary (migration too complex)
  5. Can enforce synchronized updates

Patterns & Pitfalls

Design Review Checklist

  • Semantic versioning used (MAJOR.MINOR.PATCH)
  • New fields marked optional; clients ignore unknown fields
  • Breaking changes bump MAJOR version
  • Non-breaking additions bump MINOR version
  • Deprecation timeline documented (2-3 versions/quarters)
  • Multiple API versions supported in parallel
  • Contract tests verify backward compatibility
  • Deprecated fields kept for N versions before removal
  • X-Deprecation headers or warnings sent to old clients
  • Database schema supports multiple API versions

Self-Check

Ask yourself:

  • Can I add a field to my API without breaking existing clients?
  • Do I know which API versions are in production?
  • Can old clients still work after my latest deployment?
  • How many versions back must I support?
  • Is my deprecation timeline documented and communicated?

One Key Takeaway

info

Backward compatibility requires discipline: add fields, never remove without deprecation windows; ignore unknown fields; test all versions; communicate sunsets. The cost of breaking a public API far exceeds the effort of supporting multiple versions temporarily.

Next Steps

  1. Define versioning strategy - Semantic versioning + support windows
  2. Implement version detection - Track which clients use which versions
  3. Add contract tests - Test v1, v2 clients against current API
  4. Document deprecation - Create migration guides for removals
  5. Monitor adoption - Track when v1 usage drops below threshold
  6. Plan migration - Coordinate final version removal
  7. Communicate timeline - Announce sunsets 3 months in advance

References