| name | Backward Compatibility Rules |
| description | Comprehensive guide to backward compatibility rules for APIs, databases, and data contracts with migration strategies and testing approaches |
Backward Compatibility Rules
What is Backward Compatibility?
Definition: New version works with old clients without requiring changes.
Example
API v1: GET /users → { id, name }
API v2: GET /users → { id, name, email } (added field)
Old client (expects v1) calls v2 API:
→ Receives { id, name, email }
→ Ignores 'email' field
→ Works! ✅ Backward compatible
Evolution Design
Version 1.0 → 1.1 → 1.2 → 2.0
↓ ↓ ↓ ↓
Compatible Compatible Breaking
Why Backward Compatibility Matters
1. Avoid Breaking Existing Integrations
Without Backward Compatibility:
Deploy new API version
→ All clients break immediately
→ Emergency rollback
→ Coordinate with all clients to update
→ Big bang migration (risky!)
With Backward Compatibility:
Deploy new API version
→ Old clients continue working
→ Clients migrate gradually
→ No coordination needed
2. Gradual Migration (Not Big Bang)
Timeline:
Month 0: Deploy v2 (backward compatible)
Month 1: 20% of clients migrated
Month 3: 50% of clients migrated
Month 6: 80% of clients migrated
Month 12: 100% migrated, deprecate v1
3. Reduce Coordination Overhead
Without:
- Coordinate deployment with all clients
- Schedule maintenance window
- Rollback plan if issues
With:
- Deploy anytime
- Clients migrate when ready
- No coordination needed
4. Customer Trust (Stable APIs)
Trust:
- "This API won't break my integration"
- "I can upgrade when I'm ready"
- "No surprises"
Compatibility Levels
Backward Compatible (New Server, Old Client)
Definition: Old client works with new server
Example:
Old client expects: { id, name }
New server returns: { id, name, email }
Client ignores 'email' → Works ✅
Forward Compatible (Old Server, New Client)
Definition: New client works with old server
Example:
New client expects: { id, name, email? }
Old server returns: { id, name }
Client handles missing 'email' → Works ✅
Full Compatible (Both Directions)
Definition: Works both ways
Example:
v1: { id, name }
v2: { id, name, email? } (optional)
Old client + new server: Ignores 'email' ✅
New client + old server: Handles missing 'email' ✅
Breaking Change (Incompatible)
Definition: Doesn't work
Example:
Old client expects: { id, name }
New server returns: { id, firstName, lastName }
Client looks for 'name' → Not found → Breaks ❌
REST API Backward Compatibility
Safe Changes
Add New Endpoint
Before:
GET /users
After:
GET /users
GET /projects (new)
Compatible: ✅ Old clients don't call new endpoint
Add Optional Field to Request
Before:
POST /users
{ "name": "John" }
After:
POST /users
{ "name": "John", "email": "john@example.com" } (email optional)
Compatible: ✅ Old clients don't send 'email', server handles missing field
Add Field to Response
Before:
GET /users → { "id": "123", "name": "John" }
After:
GET /users → { "id": "123", "name": "John", "email": "john@example.com" }
Compatible: ✅ Old clients ignore 'email'
Add Enum Value (at End)
Before:
status: "ACTIVE" | "INACTIVE"
After:
status: "ACTIVE" | "INACTIVE" | "PENDING"
Compatible: ✅ Old clients may not handle "PENDING", but schema is valid
Relax Validation (Accept More)
Before:
name: minLength 5
After:
name: minLength 1 (less strict)
Compatible: ✅ Old clients sending 5+ chars still work
Breaking Changes
Remove Endpoint
Before:
GET /users
After:
(endpoint removed)
Breaking: ❌ Old clients calling endpoint get 404
Remove Field from Response
Before:
GET /users → { "id": "123", "name": "John" }
After:
GET /users → { "id": "123" } ('name' removed)
Breaking: ❌ Old clients expect 'name' field
Add Required Field to Request
Before:
POST /users
{ "name": "John" }
After:
POST /users
{ "name": "John", "email": "john@example.com" } (email required)
Breaking: ❌ Old clients don't send 'email', get 400 error
Change Field Type
Before:
age: integer
After:
age: string
Breaking: ❌ Old clients send integer, server expects string
Rename Field
Before:
{ "name": "John" }
After:
{ "fullName": "John" }
Breaking: ❌ Old clients look for 'name', not found
Change Semantics
Before:
amount: 100 (dollars)
After:
amount: 10000 (cents, changed meaning!)
Breaking: ❌ Same field, different meaning
Database Schema Compatibility
Safe Migrations
Add Nullable Column
ALTER TABLE users ADD COLUMN email VARCHAR(255);
Add Table
CREATE TABLE projects (
id UUID PRIMARY KEY,
name VARCHAR(255)
);
Add Index
CREATE INDEX idx_users_email ON users(email);
Breaking Migrations
Drop Column (Requires Multi-Step Migration)
ALTER TABLE users DROP COLUMN name;
Rename Column (Use Views/Aliases)
ALTER TABLE users RENAME COLUMN name TO full_name;
ALTER TABLE users ADD COLUMN full_name VARCHAR(255);
UPDATE users SET full_name = name;
CREATE VIEW users_v1 AS
SELECT id, full_name AS name FROM users;
Change Column Type
ALTER TABLE users ALTER COLUMN age TYPE VARCHAR(10);
ALTER TABLE users ADD COLUMN age_str VARCHAR(10);
UPDATE users SET age_str = age::VARCHAR;
Event/Message Compatibility
Safe Changes
Add Optional Field
Before:
{ "id": "123", "name": "John" }
After:
{ "id": "123", "name": "John", "email": "john@example.com" }
Compatible: ✅ Old consumers ignore 'email'
Add New Event Type
Before:
user.created
After:
user.created
user.deleted (new)
Compatible: ✅ Old consumers don't subscribe to new event
Breaking Changes
Remove Field
Before:
{ "id": "123", "name": "John" }
After:
{ "id": "123" } ('name' removed)
Breaking: ❌ Old consumers expect 'name'
Change Field Type
Before:
{ "age": 30 } (integer)
After:
{ "age": "30" } (string)
Breaking: ❌ Old consumers expect integer
Rename Field
Before:
{ "name": "John" }
After:
{ "fullName": "John" }
Breaking: ❌ Old consumers look for 'name'
GraphQL Compatibility
Safe Changes
Add New Field to Type
Before:
type User {
id: ID!
name: String!
}
After:
type User {
id: ID!
name: String!
email: String
}
Compatible: ✅ Old queries don't request 'email'
Add New Type
After:
type Project {
id: ID!
name: String!
}
Compatible: ✅ Old queries don't use new type
Add New Query/Mutation
After:
type Query {
users: [User!]!
projects: [Project!]!
}
Compatible: ✅ Old clients don't call new query
Breaking Changes
Remove Field
Before:
type User {
id: ID!
name: String!
}
After:
type User {
id: ID!
}
Breaking: ❌ Old queries requesting 'name' fail
Change Field Type
Before:
type User {
age: Int!
}
After:
type User {
age: String!
}
Breaking: ❌ Old queries expect Int
Change Field Arguments
Before:
users(limit: Int): [User!]!
After:
users(limit: Int!, offset: Int!): [User!]!
Breaking: ❌ Old queries don't provide 'offset'
Deprecation Strategy
Mark as Deprecated
In Docs:
## GET /users/:id/profile
**Deprecated:** Use GET /users/:id instead.
**Sunset Date:** 2024-12-31
In Code:
function getUserProfile(id) {
}
In Headers:
HTTP/1.1 200 OK
Deprecation: true
Sunset: Sat, 31 Dec 2024 23:59:59 GMT
Link: <https://docs.example.com/migration>; rel="deprecation"
Provide Migration Guide
Example:
# Migration Guide: /users/:id/profile → /users/:id
## What's Changing
The `/users/:id/profile` endpoint is deprecated. Use `/users/:id` instead.
## Why
Consolidating user endpoints for simplicity.
## How to Migrate
### Before
```javascript
const profile = await fetch('/api/users/123/profile');
After
const user = await fetch('/api/users/123');
Timeline
- 2024-06-01: Deprecation announced
- 2024-12-31: Endpoint will stop working
### Set Sunset Date (6-12 Months)
**Timeline:**
Month 0: Announce deprecation
Month 3: Warn users (emails, headers)
Month 6: Final warning
Month 12: Sunset (stop working)
### Warn Clients (in Response Headers)
HTTP/1.1 200 OK
Deprecation: true
Sunset: Sat, 31 Dec 2024 23:59:59 GMT
Warning: 299 - "This endpoint is deprecated. Use /v2/users instead. See https://docs.example.com/migration"
### Monitor Usage
**Track Deprecated Endpoint Usage:**
```javascript
app.get('/users/:id/profile', (req, res) => {
// Log usage
metrics.increment('deprecated.users_profile.calls');
// Return response with deprecation headers
res.set('Deprecation', 'true');
res.set('Sunset', 'Sat, 31 Dec 2024 23:59:59 GMT');
res.json(profile);
});
Dashboard:
Deprecated Endpoint Usage:
/users/:id/profile
- Calls/day: 1,000 (down from 5,000 last month)
- Top clients: mobile-app (500), web-app (300), partner-api (200)
- Sunset date: 2024-12-31 (6 months remaining)
Remove After Sunset
Process:
1. Sunset date reached
2. Return 410 Gone
3. Monitor for errors
4. After 1 month, remove code
410 Gone Response:
HTTP/1.1 410 Gone
Content-Type: application/json
{
"error": "This endpoint has been removed. Use /v2/users instead.",
"migrationGuide": "https://docs.example.com/migration"
}
Multi-Step Migrations
Example: Rename Field
Step 1: Add New Field, Both Fields Populated
{
"name": "John Doe",
"fullName": "John Doe"
}
Step 2: Clients Migrate to New Field
Month 1-3: Clients update to use 'fullName'
Step 3: Stop Populating Old Field (Still Present)
{
"name": null,
"fullName": "John Doe"
}
Step 4: Remove Old Field
{
"fullName": "John Doe"
}
API Versioning Strategies
URL Versioning (/v1/, /v2/)
Format:
/api/v1/users
/api/v2/users
Pros:
- Clear and visible
- Easy to route
- Can run multiple versions
Cons:
- URL changes
- Maintain multiple codebases
Header Versioning (Accept-Version: v1)
Format:
GET /api/users
Accept-Version: v1
Pros:
- URL doesn't change
- More RESTful
Cons:
- Less visible
- Harder to test
No Versioning (Only Non-Breaking Changes)
Strategy:
- Never make breaking changes
- Always backward compatible
Pros:
Cons:
- Technical debt accumulates
Testing Backward Compatibility
Contract Tests (Pact)
See Contract Testing skill for details
Integration Tests with Old Clients
Example:
describe('Backward compatibility', () => {
test('v1 client works with v2 API', async () => {
const response = await v1Client.get('/users/123');
expect(response).toHaveProperty('id');
expect(response).toHaveProperty('name');
});
});
Schema Compatibility Checks (Automated)
OpenAPI:
oasdiff breaking old.yaml new.yaml
JSON Schema:
const isCompatible = checkBackwardCompatibility(oldSchema, newSchema);
if (!isCompatible) {
throw new Error('Breaking change detected');
}
Canary Deployments
Process:
1. Deploy new version to 5% of traffic
2. Monitor errors
3. If no errors, increase to 25%
4. Continue until 100%
Compatibility Checking Tools
OpenAPI: oasdiff, openapi-diff
See OpenAPI Governance skill
GraphQL: graphql-inspector
Install:
npm install -g @graphql-inspector/cli
Check:
graphql-inspector diff old.graphql new.graphql
Output:
✖ Field 'name' was removed from object type 'User' (BREAKING)
✔ Field 'email' was added to object type 'User'
Protobuf: buf breaking
Install:
brew install bufbuild/buf/buf
Check:
buf breaking --against .git#branch=main
JSON Schema: schema validators
Example:
function checkBackwardCompatibility(oldSchema, newSchema) {
for (const field of oldSchema.required) {
if (!newSchema.required.includes(field)) {
return false;
}
}
for (const [field, oldType] of Object.entries(oldSchema.properties)) {
const newType = newSchema.properties[field];
if (newType && newType.type !== oldType.type) {
return false;
}
}
return true;
}
Communicating Breaking Changes
Changelog (What Changed)
Example:
# Changelog
## v2.0.0 (2024-06-01) - BREAKING CHANGES
### Breaking Changes
- **Removed:** `GET /users/:id/profile` endpoint. Use `GET /users/:id` instead.
- **Changed:** `POST /users` now requires `email` field.
- **Renamed:** Response field `name` renamed to `fullName`.
### Migration Guide
See https://docs.example.com/migration/v1-to-v2
## v1.2.0 (2024-05-01)
### Added
- New field `avatar` in user response (optional, backward compatible)
Migration Guide (How to Update)
See "Provide Migration Guide" section above
Deprecation Notice (When Will It Break)
See "Deprecation Strategy" section above
Direct Outreach (Email Major Clients)
Email Template:
Subject: Action Required: API v1 Deprecation
Hi [Client],
We're writing to inform you that API v1 will be deprecated on December 31, 2024.
What's changing:
- Endpoint /users/:id/profile will be removed
- Use /users/:id instead
Action required:
- Update your integration by December 31, 2024
- See migration guide: https://docs.example.com/migration
Need help?
- Reply to this email
- Schedule a call: https://calendly.com/api-team
Thank you,
API Team
Handling Inevitable Breaking Changes
Major Version Bump (v1 → v2)
Process:
1. Create v2 API (breaking changes)
2. Maintain v1 API (for transition)
3. Announce deprecation of v1
4. Give 6-12 months notice
5. Sunset v1
Maintain v1 for Transition Period
Timeline:
Month 0: Launch v2
Month 0-12: Maintain both v1 and v2
Month 12: Sunset v1
Clear Migration Path
Provide:
- Migration guide (step-by-step)
- Code examples (before/after)
- Tools to help migrate
- Support (email, calls)
Tools to Help Migrate
Example:
./migrate-to-v2.sh
Real-World Examples
Stripe API Evolution
Versioning:
- Header-based:
Stripe-Version: 2024-01-15
- Backward compatible within version
- Breaking changes = new version
Deprecation:
- 6-12 months notice
- Detailed migration guides
- Direct outreach to major customers
AWS API Backward Compatibility
Policy:
- Never break existing APIs
- Add new APIs for new features
- Deprecate old APIs (but keep working)
GraphQL Schema Evolution
Best Practices:
- Add fields (don't remove)
- Deprecate fields (don't remove immediately)
- Use
@deprecated directive
Best Practices Checklist
- [ ] All changes reviewed for compatibility
- [ ] Automated compatibility checks in CI
- [ ] Deprecation process defined (6-12 months)
- [ ] Migration guides for breaking changes
- [ ] Versioning strategy documented
- [ ] Monitor deprecated endpoint usage
- [ ] Direct outreach to major clients
- [ ] Test with old clients
- [ ] Canary deployments for risky changes
Implementation
Compatibility Check Scripts
See "Compatibility Checking Tools" section
CI/CD Integration
GitHub Actions:
- name: Check backward compatibility
run: |
# OpenAPI
oasdiff breaking origin/main:openapi.yaml HEAD:openapi.yaml
graphql-inspector diff origin/main:schema.graphql HEAD:schema.graphql
if [ $? -ne 0 ]; then
echo "Breaking changes detected"
exit 1
fi
Deprecation Header Middleware
Express:
function deprecationMiddleware(sunsetDate, migrationUrl) {
return (req, res, next) => {
res.set('Deprecation', 'true');
res.set('Sunset', sunsetDate);
res.set('Link', `<${migrationUrl}>; rel="deprecation"`);
res.set('Warning', `299 - "This endpoint is deprecated. See ${migrationUrl}"`);
metrics.increment('deprecated.endpoint.calls', {
path: req.path,
client: req.headers['user-agent']
});
next();
};
}
app.get('/users/:id/profile',
deprecationMiddleware('Sat, 31 Dec 2024 23:59:59 GMT', 'https://docs.example.com/migration'),
(req, res) => {
}
);
Summary
Quick Reference
Backward Compatibility: New version works with old clients
Why:
- Avoid breaking integrations
- Gradual migration
- Reduce coordination
- Customer trust
Safe Changes (REST API):
- Add endpoint
- Add optional field to request
- Add field to response
- Add enum value
- Relax validation
Breaking Changes:
- Remove endpoint/field
- Add required field to request
- Change field type
- Rename field
- Change semantics
Database:
- Safe: Add nullable column, add table, add index
- Breaking: Drop column, rename column, change type
Deprecation:
- Mark as deprecated
- Migration guide
- 6-12 months notice
- Monitor usage
- Remove after sunset
Multi-Step Migration:
- Add new field, populate both
- Clients migrate
- Stop populating old field
- Remove old field
Versioning:
- URL:
/v1/, /v2/
- Header:
Accept-Version: v1
- No versioning (only non-breaking)
Testing:
- Contract tests (Pact)
- Integration tests with old clients
- Schema compatibility checks
- Canary deployments
Tools:
- OpenAPI: oasdiff
- GraphQL: graphql-inspector
- Protobuf: buf breaking
- JSON Schema: custom validators
Communicating:
- Changelog
- Migration guide
- Deprecation notice
- Direct outreach
Best Practices
Version Control
[ ] Document version compatibility matrix
[ ] Use @deprecated directive for breaking changes
Communication
[ ] Create data contracts with consumers
[ ] Document breaking changes clearly
[ ]
Testing
[ ] Test migration scripts thoroughly
[ ]
Migration
[ ] Backfill data before removing old columns
[ ]
Documentation
[ ] Document all breaking changes with dates
[ ] Document breaking changes clearly
[ ] Provide migration guides for consumers
[ ] Document version compatibility
[ ]
Monitoring
[ ] Monitor breaking change impacts
[ ] Track consumer adoption of new schema
[ ] Set up dashboards for schema health
[ ] [ ]
Prevention
[ ] Use data contracts for all shared data
[ ] Enforce schema validation at source
[ ] [ ] Implement CI/CD schema checks
[ ]
Checklist
[ ] Use semantic versioning
[ ] Implement zero-downtime migrations
[ ] Backfill data before removing old columns
[ ] Test backward compatibility
[ ] Have rollback procedures ready
[ ] Monitor schema drift metrics
[ ] Track migration success rates
[ ] Document all breaking changes
[ ] Set up change notifications
[ ] Test with production-like data
[ ] Monitor test coverage
[ ] Optimize schema validation overhead
[ ] Cache schema definitions
[ ] Use efficient validation libraries
[ ] Monitor schema performance impact
[ ] Establish schema ownership
[ ] Create schema review process
[ ] Define schema lifecycle
[ ] Plan schema deprecation strategy
[ ] Set up incident response for violations
[ ] Test schema validation logic
[ ] Train team on backward compatibility
[ ]