Claude-skill-registry Backward Compatibility Rules
Comprehensive guide to backward compatibility rules for APIs, databases, and data contracts with migration strategies and testing approaches
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/backward-compat-rules" ~/.claude/skills/majiayu000-claude-skill-registry-backward-compatibility-rules && rm -rf "$T"
skills/data/backward-compat-rules/SKILL.mdBackward 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
-- Safe: Existing rows get NULL ALTER TABLE users ADD COLUMN email VARCHAR(255);
Add Table
-- Safe: Doesn't affect existing tables CREATE TABLE projects ( id UUID PRIMARY KEY, name VARCHAR(255) );
Add Index
-- Safe: Improves performance, doesn't change data CREATE INDEX idx_users_email ON users(email);
Breaking Migrations
Drop Column (Requires Multi-Step Migration)
-- Breaking: Existing queries fail ALTER TABLE users DROP COLUMN name; -- Multi-step migration: -- Step 1: Stop writing to column -- Step 2: Deploy code that doesn't read column -- Step 3: Drop column
Rename Column (Use Views/Aliases)
-- Breaking: Existing queries fail ALTER TABLE users RENAME COLUMN name TO full_name; -- Multi-step migration: -- Step 1: Add new column ALTER TABLE users ADD COLUMN full_name VARCHAR(255); -- Step 2: Copy data UPDATE users SET full_name = name; -- Step 3: Create view with old name CREATE VIEW users_v1 AS SELECT id, full_name AS name FROM users; -- Step 4: Migrate clients to new column -- Step 5: Drop view and old column
Change Column Type
-- Breaking: Data may not fit ALTER TABLE users ALTER COLUMN age TYPE VARCHAR(10); -- Multi-step migration: -- Step 1: Add new column ALTER TABLE users ADD COLUMN age_str VARCHAR(10); -- Step 2: Copy and convert data UPDATE users SET age_str = age::VARCHAR; -- Step 3: Migrate clients -- Step 4: Drop old column, rename new column
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 # New field } 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!]! # New query } Compatible: ✅ Old clients don't call new query
Breaking Changes
Remove Field
Before: type User { id: ID! name: String! } After: type User { id: ID! # 'name' removed } Breaking: ❌ Old queries requesting 'name' fail
Change Field Type
Before: type User { age: Int! } After: type User { age: String! # Changed type } Breaking: ❌ Old queries expect Int
Change Field Arguments
Before: users(limit: Int): [User!]! After: users(limit: Int!, offset: Int!): [User!]! # 'offset' required 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:
/** * @deprecated Use getUser() instead. Will be removed in v2.0.0. */ 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
// API returns both fields { "name": "John Doe", // Old field (deprecated) "fullName": "John Doe" // New field }
Step 2: Clients Migrate to New Field
Month 1-3: Clients update to use 'fullName'
Step 3: Stop Populating Old Field (Still Present)
// API returns both fields, but old field is null { "name": null, // Old field (deprecated, not populated) "fullName": "John Doe" // New field }
Step 4: Remove Old Field
// API returns only new field { "fullName": "John Doe" // New field }
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:
- Simple
- No migration
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 () => { // v1 client expects { id, name } const response = await v1Client.get('/users/123'); expect(response).toHaveProperty('id'); expect(response).toHaveProperty('name'); // v2 API may return additional fields (email), but v1 client ignores them }); });
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) { // Check if all required fields in old schema are still required for (const field of oldSchema.required) { if (!newSchema.required.includes(field)) { return false; // Breaking: required field removed } } // Check if field types changed for (const [field, oldType] of Object.entries(oldSchema.properties)) { const newType = newSchema.properties[field]; if (newType && newType.type !== oldType.type) { return false; // Breaking: type changed } } return true; // Compatible }
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:
# Migration tool ./migrate-to-v2.sh # Scans code for v1 API calls # Suggests v2 equivalents # Optionally auto-updates code
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
directive@deprecated
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 graphql-inspector diff origin/main:schema.graphql HEAD:schema.graphql # Fail if breaking changes 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}"`); // Log usage metrics.increment('deprecated.endpoint.calls', { path: req.path, client: req.headers['user-agent'] }); next(); }; } // Usage app.get('/users/:id/profile', deprecationMiddleware('Sat, 31 Dec 2024 23:59:59 GMT', 'https://docs.example.com/migration'), (req, res) => { // Handle request } );
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
-
Use semantic versioning (SemVer)
-
Tag all releases with version numbers
[ ] Document version compatibility matrix
- Maintain backward compatibility
[ ] Use @deprecated directive for breaking changes
- [ ]
Communication
-
Communicate changes early
-
Provide migration guides
-
Hold breaking change meetings
[ ] Create data contracts with consumers
[ ] Document breaking changes clearly
[ ]
Testing
-
Test backward compatibility
-
Test with production-like data
[ ] Test migration scripts thoroughly
- Monitor test coverage
[ ]
Migration
-
Use zero-downtime migration pattern
-
Use multi-step migrations for breaking changes
[ ] Backfill data before removing old columns
- Test migrations in staging environment
[ ]
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
[ ]