Split View: API 버저닝 & 진화 전략 완전 가이드 2025: Breaking Changes 없는 API 진화, Deprecation, Sunset
API 버저닝 & 진화 전략 완전 가이드 2025: Breaking Changes 없는 API 진화, Deprecation, Sunset
TL;DR
- 버저닝 전략 4가지: URL Path (
/v1/), Header (API-Version: 2024-01-01), Content Negotiation (Accept: application/vnd.api+json;v=1), Query (?version=1) - Stripe 방식 = 날짜 기반:
2024-04-15처럼 날짜로 버전. 가장 우아한 접근 - GitHub 방식 = REST URL:
/v3/repos. 단순하지만 메이저 변경마다 마이그레이션 필요 - GraphQL = 버전 없음: 필드 단위 deprecation으로 진화. Twitter, GitHub, Shopify 채택
- Sunset 헤더: RFC 8594.
Sunset: Sat, 31 Dec 2025 23:59:59 GMT— 클라이언트에게 폐기 일정 알림
1. API 진화의 본질적 어려움
1.1 외부 API의 운명
"한 번 공개된 API는 영원히 살아있다." — Hyrum's Law
내부 코드는 마음대로 리팩토링할 수 있습니다. 외부 API는 다릅니다:
- 수천~수백만 클라이언트가 의존
- 모바일 앱은 사용자가 업데이트해야 새 버전
- 통합된 비즈니스 시스템은 변경에 인색
- "잠시 멈춤"이 매출 손실로 직결
1.2 변경의 종류
| 변경 | 영향 | 호환성 |
|---|---|---|
| 새 엔드포인트 추가 | 없음 | ✅ |
| 새 응답 필드 추가 | 없음 | ✅ |
| 선택 필드 추가 | 없음 | ✅ |
| 응답 필드 제거 | Breaking | ❌ |
| 필드 이름 변경 | Breaking | ❌ |
| 필드 타입 변경 | Breaking | ❌ |
| 필수 필드 추가 | Breaking | ❌ |
| 에러 코드 의미 변경 | Breaking | ❌ |
| 기본 동작 변경 | Breaking | ❌ |
규칙: 추가는 안전, 제거/변경은 위험.
1.3 Hyrum's Law의 잔혹함
"API에 충분한 사용자가 있으면, 어떤 관찰 가능한 동작도 누군가는 의존한다."
실제 사례:
- 응답 필드 순서를 가정하는 클라이언트
- 에러 메시지의 정확한 텍스트를 파싱하는 클라이언트
- 부수 효과(예: ID가 순차적인 것)에 의존하는 클라이언트
- 응답 시간이 일정하다는 가정
결론: "공식 문서에 없는 것은 바꿔도 된다"는 잘못입니다. 모든 관찰 가능한 동작이 API의 일부입니다.
2. 버저닝 전략 4가지
2.1 URL Path Versioning
GET /v1/users/123
GET /v2/users/123
장점:
- 가장 명확
- 디버깅 쉬움 (URL만 봐도 버전 보임)
- 캐시 친화적 (다른 URL = 다른 캐시)
단점:
- 메이저 변경마다 새 URL 필요
- 각 버전을 별도 코드베이스로 유지
- 클라이언트가 URL을 하드코딩
채택: GitHub (/v3/), Twitter, Stripe (URL은 /v1/이지만 실제는 헤더로)
2.2 Header Versioning
GET /users/123 HTTP/1.1
Stripe-Version: 2024-04-15
장점:
- URL이 깨끗
- 같은 리소스, 다른 표현
- 점진적 마이그레이션 쉬움
단점:
- 디버깅 어려움 (헤더 안 보이면)
- 캐시 처리 복잡 (Vary 헤더)
- 클라이언트 라이브러리가 헤더 자동 추가해야 함
채택: Stripe (가장 유명), Azure
2.3 Content Negotiation
GET /users/123 HTTP/1.1
Accept: application/vnd.example.user.v2+json
장점:
- HTTP 표준 (Accept 헤더 활용)
- 같은 URL로 다른 버전
- HATEOAS와 잘 맞음
단점:
- 복잡함
- 클라이언트가 정확한 MIME 타입 알아야 함
채택: GitHub (선택적, Accept: application/vnd.github.v3+json)
2.4 Query Parameter
GET /users/123?version=2
GET /users/123?api_version=2024-04-15
장점:
- 가장 단순
- URL에 보임 (디버깅 쉬움)
단점:
- "데이터의 일부"처럼 보임 (실제로는 메타데이터)
- 캐시 처리 복잡
채택: 일부 단순 API
2.5 비교표
| 방식 | URL 깨끗 | 디버깅 | 캐시 | 채택 |
|---|---|---|---|---|
| URL Path | ❌ | ⭐⭐⭐ | ⭐⭐⭐ | GitHub, Twitter |
| Header | ⭐⭐⭐ | ⭐ | ⭐⭐ | Stripe, Azure |
| Content Negotiation | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐ | GitHub (옵션) |
| Query Param | ⭐⭐ | ⭐⭐⭐ | ⭐⭐ | 단순 API |
3. Stripe의 천재적인 날짜 기반 버저닝
3.1 핵심 아이디어
Stripe는 버전을 날짜로 표현합니다:
2024-04-15(해당 날짜의 API 동작)2023-10-16(이전 동작)2020-08-27(5년 전 동작)
모든 변경은 날짜로 식별됩니다.
3.2 클라이언트 사용
import stripe
# 계정 기본 버전 사용
stripe.api_version = "2024-04-15"
# 또는 요청별
stripe.Charge.create(
amount=2000,
currency="usd",
api_version="2023-10-16" # 옛 동작
)
3.3 Stripe의 비결 — 변환 레이어
서버에는 단 하나의 코드베이스 (최신 버전).
각 요청에 대해:
- 클라이언트 버전 확인
- 요청을 최신 버전으로 변환 (forward transform)
- 처리
- 응답을 클라이언트 버전으로 변환 (backward transform)
클라이언트 (v2020) → [변환] → 최신 코드 → [변환] → 클라이언트 (v2020)
효과:
- 5년 전 클라이언트도 동작
- 코드는 최신 상태 유지
- 새 기능은 즉시 모든 사용자에게 (옵트인)
3.4 변환 예시
v2020에서 v2024 변경: 응답에 description 필드 추가.
# 변환 함수
def transform_to_v2020(response):
if "description" in response:
del response["description"] # v2020 클라이언트는 이 필드를 모름
return response
v2020에서 v2024 변경: amount가 정수에서 객체로 변경.
def transform_to_v2020(response):
if isinstance(response.get("amount"), dict):
response["amount"] = response["amount"]["value"]
return response
3.5 Stripe 변경 로그
각 버전 변경이 정확히 문서화됩니다:
2024-04-15
- Added
descriptionfield toChargeobjectamountfield type changed from integer to AmountObject- Default
currencyis now derived from account settingsMigration guide: ...
이 정도의 투명성이 신뢰의 핵심입니다.
4. GraphQL의 버전 없는 진화
4.1 핵심 철학
GraphQL은 버전을 사용하지 않습니다. 대신 필드 단위로 진화:
- 추가: 새 필드 추가 — 기존 클라이언트는 모르므로 영향 없음
- 삭제:
@deprecated마크 후 일정 시간 후 제거
type User {
id: ID!
name: String!
# Deprecated 필드
email: String @deprecated(reason: "Use 'emailAddress' instead. Will be removed 2025-12-31")
emailAddress: String!
}
4.2 클라이언트가 정확히 요청
query {
user(id: "123") {
id
name
emailAddress # 새 필드만 요청
}
}
기존 클라이언트는 email을 계속 요청 → 작동. 새 클라이언트는 emailAddress 사용.
Over-fetching이 없다 = 새 필드 추가가 무료.
4.3 Deprecation 추적
GraphQL 서버가 사용 통계를 수집:
- 어떤 클라이언트가
email을 사용 중인가? - 마지막 사용은 언제?
- 안전하게 제거 가능한가?
Apollo Studio, Hasura Cloud가 이 기능 제공.
4.4 GraphQL의 한계
- 모든 변경이 호환되는 건 아님 — 타입 변경, enum 값 제거 등은 여전히 breaking
- 클라이언트 코드 생성 — 새 스키마로 다시 빌드 필요
- 고급 도구 필요 — 사용 추적 등
채택: GitHub, Shopify, Twitter, Airbnb
5. Semantic Versioning과 API
5.1 SemVer 기본
MAJOR.MINOR.PATCH
v1.2.3
- MAJOR: 호환성 깨짐 (breaking)
- MINOR: 호환성 유지 + 새 기능
- PATCH: 호환성 유지 + 버그 수정
5.2 라이브러리 vs API
라이브러리: SemVer 자연스러움.
npm install foo@^1.0.0→ 1.x.x 자동 업데이트
Web API: 적용 어려움.
- 클라이언트가 자동 업데이트 못 함
- "v1.2"와 "v1.3"의 차이는 의미 있지만, "v1.2.3"과 "v1.2.4"는 거의 의미 없음
현실: Web API는 보통 메이저 버전만 표시 (/v1, /v2).
5.3 SemVer의 한계
v2.0.0이 나오면 모든 사용자가 마이그레이션 필요. 점진적 진화가 어려움.
→ Stripe 방식 (날짜 기반) 또는 **GraphQL 방식 (버전 없음)**이 더 우아.
6. Deprecation과 Sunset
6.1 Deprecation 단계
- Announce: 블로그, 이메일, 변경 로그
- Mark in API: 응답 헤더 또는 필드
- Monitor: 사용 추적
- Reminder: 사용 클라이언트에 직접 알림
- Sunset: 폐기 (HTTP 410 Gone)
6.2 Deprecation 헤더
RFC 8594: HTTP Sunset 헤더
HTTP/1.1 200 OK
Sunset: Sat, 31 Dec 2025 23:59:59 GMT
Deprecation: Sat, 31 Dec 2024 23:59:59 GMT
Link: <https://api.example.com/docs/migration>; rel="deprecation"
의미:
Deprecation: 이미 deprecated (아직 작동)Sunset: 폐기 일정Link: 마이그레이션 가이드
6.3 Deprecation 메시지 (응답 본문)
{
"data": {...},
"warnings": [
{
"code": "DEPRECATED_FIELD",
"message": "Field 'email' is deprecated. Use 'emailAddress' instead.",
"documentation_url": "https://api.example.com/docs/v2#email-deprecation",
"sunset_date": "2025-12-31"
}
]
}
6.4 사용자에게 알리기
기술적:
- 응답 헤더 (
Sunset,Deprecation) - 응답 본문의 warnings 필드
커뮤니케이션:
- 이메일 (등록된 개발자)
- 블로그 / 변경 로그
- 대시보드 공지
- 직접 연락 (큰 사용자)
Stripe: 사용 중인 deprecated API에 대해 자동 이메일 발송.
6.5 Sunset 정책 예시
| 사용자 | Sunset 기간 |
|---|---|
| 무료 사용자 | 6개월 |
| 유료 사용자 | 1년 |
| 엔터프라이즈 | 2년 |
큰 회사일수록 변경에 시간 필요.
7. Breaking Changes 회피 전략
7.1 Additive Changes 우선
잘못된 변경:
- "user_email"
+ "email"
올바른 변경:
+ "email" // 새 필드 추가
"user_email" // 옛 필드 유지 (deprecated)
두 필드를 같이 반환. 클라이언트가 마이그레이션할 시간 줌.
7.2 새 엔드포인트 vs 기존 변경
Bad: 기존 /users 응답 형식 변경.
Good: 새 /v2/users 엔드포인트 또는 /users?format=new.
7.3 Default 값 신중히
// v1
{ "page_size": 20 } // 기본값 20
// v2 — 50으로 변경?
{ "page_size": 50 } // ❌ Breaking! (페이지네이션 동작 변경)
기본값 변경은 종종 breaking입니다.
7.4 Optional → Required는 Breaking
- email: string? // optional
+ email: string // required
기존 클라이언트가 email을 보내지 않으면 실패. 추가하지 마세요.
7.5 Enum 값 추가는 안전?
enum Status {
ACTIVE,
INACTIVE,
+ PENDING_REVIEW // 새 값 추가
}
미묘함: 클라이언트가 enum을 switch로 처리하면 새 값에 대한 default 케이스가 필요. 있다면 안전, 없다면 미묘한 버그.
조언: enum 추가는 기술적으로 backward compatible이지만, 클라이언트 코드 검토 필요.
8. 실제 API의 진화 사례
8.1 Stripe — 날짜 기반의 우아함
- 10년+ API 진화
- 모든 변경에 정확한 날짜
- 클라이언트는 자기 페이스로 업그레이드
- 사용 통계 + 자동 알림
8.2 GitHub — REST에서 GraphQL로
- REST v3: 2014년부터 운영
- GraphQL v4: 2017년 출시
- 두 API 병행 운영
- 새 기능은 GraphQL 우선
교훈: 기존 API는 그대로 두고, 새 패러다임은 별도 시작.
8.3 Twilio — 메이저 버전 + 점진적 마이그레이션
/2008-08-01/,/2010-04-01/처럼 날짜 prefix- 하지만 메이저 변경은 새 prefix
- 옛 버전은 수년간 유지
8.4 Slack — 점진적 deprecation
- 빈번한 새 메서드 추가
- Deprecation은 6개월~1년 공지
- 사용자에게 직접 이메일
8.5 AWS — 거의 절대 깨지 않음
- 2006년 출시된 S3 API가 여전히 작동
- 새 기능은 추가만, 기존은 절대 변경 안 함
- 결과: API가 일관성 없고 복잡하지만 호환성 완벽
9. 모범 사례 체크리스트
9.1 설계 단계
- 버저닝 전략 결정 (URL/Header/날짜)
- 명확한 SLA (몇 년 지원할 것인가?)
- Deprecation 정책 문서화
- 변경 로그 자동화
9.2 변경 시
- Breaking change인가? (체크리스트로 확인)
- Additive로 만들 수 있나?
- 마이그레이션 가이드 작성
- Sunset 헤더 추가
- 사용자에게 공지
- 사용 통계 모니터링
9.3 Sunset 시
- 사용자 0명까지 대기
- 마지막 알림
- HTTP 410 Gone 응답
- 코드 제거 (시간이 지난 후)
10. API 진화의 미래
10.1 OpenAPI 3.1 + JSON Schema
스키마를 통한 자동 호환성 검사:
- API spec 변경 시 자동으로 breaking change 감지
- 클라이언트 코드 자동 생성
10.2 AI 기반 마이그레이션
- AI가 코드 분석 → 자동 마이그레이션 PR 생성
- 변경 영향도 자동 분석
10.3 contract testing 표준화
- Pact, Spring Cloud Contract 같은 도구
- API 제공자와 소비자 간 계약 강제
- CI에서 호환성 검증
퀴즈
1. 가장 흔한 Breaking Change는?
답: 응답 필드 제거 또는 이름 변경입니다. 클라이언트가 그 필드를 사용 중이라면 즉시 깨집니다. 안전한 대안: 새 필드를 추가하고 기존 필드를 deprecated로 표시 (둘 다 반환). 일정 기간 후 제거. 다른 흔한 breaking changes: 필드 타입 변경(string → object), 필수 필드 추가, 기본값 변경, enum 값 의미 변경.
2. Stripe의 날짜 기반 버저닝의 장점은?
답: (1) 점진적 마이그레이션 — 클라이언트가 자기 페이스로 새 버전 채택, (2) 단일 코드베이스 — 서버는 최신 버전만 유지, 변환 레이어로 옛 클라이언트 지원, (3) 명확한 변경 로그 — 각 날짜의 변경 사항이 정확히 문서화, (4) 테스트 용이 — 특정 날짜 버전을 명시적으로 테스트. 단점은 변환 레이어 구현이 복잡하다는 것.
3. GraphQL이 버전을 사용하지 않는 이유는?
답: GraphQL은 클라이언트가 정확히 필요한 필드만 요청하므로, 새 필드 추가가 기존 클라이언트에 영향 없습니다 — 그들은 그 필드를 요청하지 않으니까. 필드 제거는 @deprecated로 마크하고, 사용 통계를 추적하여 안전하게 제거 가능. 결과: 버전 없이 영원히 진화하는 API. 단점은 모든 변경이 호환되는 건 아니라는 것 (타입 변경, enum 값 제거 등).
4. Sunset 헤더의 역할은?
답: RFC 8594 표준 HTTP 헤더로, "이 리소스가 언제 폐기될 것인가"를 알려줍니다. Sunset: Sat, 31 Dec 2025 23:59:59 GMT. 클라이언트는 이 헤더를 보고 마이그레이션 일정을 자동으로 인식할 수 있습니다. Deprecation 헤더와 Link 헤더(마이그레이션 가이드)와 함께 사용. 자동화된 클라이언트가 안전하게 폐기 일정에 대응할 수 있게 합니다.
5. Hyrum's Law가 API 설계에 의미하는 것은?
답: "API에 충분한 사용자가 있으면, 어떤 관찰 가능한 동작도 누군가는 의존한다." 즉, 공식 문서에 없어도 변경하면 안 됩니다. 응답 필드 순서, 에러 메시지의 정확한 텍스트, 응답 시간, ID의 순차성 등 모두가 "API의 일부"가 됩니다. 결론: (1) 처음부터 신중하게 설계, (2) 변경 시 모든 관찰 가능한 동작을 고려, (3) 의도하지 않은 동작은 명시적으로 "이 동작에 의존하지 마세요"라고 문서화.
참고 자료
- Stripe API Versioning — 날짜 기반 접근 설명
- GitHub API v3 to v4 — GraphQL 마이그레이션
- RFC 8594 — Sunset Header
- Hyrum's Law
- OpenAPI 3.1 — API 명세 표준
- Pact — Contract testing
- GraphQL Best Practices
- API Stylebook — 회사별 API 가이드 모음
- Twilio API Versioning
- Building Evolvable Web APIs — Glenn Block et al.
- API Change Management at Stripe
API Versioning & Evolution Strategy Complete Guide 2025: Breaking-Change-Free API Evolution, Deprecation, Sunset
TL;DR
- Four versioning strategies: URL Path (
/v1/), Header (API-Version: 2024-01-01), Content Negotiation (Accept: application/vnd.api+json;v=1), Query (?version=1) - Stripe's approach = date-based: versions like
2024-04-15. The most elegant approach - GitHub's approach = REST URL:
/v3/repos. Simple but requires migration on every major change - GraphQL = no version: evolves via field-level deprecation. Adopted by Twitter, GitHub, Shopify
- Sunset header: RFC 8594.
Sunset: Sat, 31 Dec 2025 23:59:59 GMT— notifies clients of the retirement schedule
1. The Intrinsic Difficulty of API Evolution
1.1 The Fate of Public APIs
"Once an API is public, it lives forever." — Hyrum's Law
Internal code can be refactored freely. External APIs are different:
- Thousands to millions of clients depend on it
- Mobile apps require users to update to get the new version
- Integrated business systems resist change
- A "brief outage" translates directly into lost revenue
1.2 Types of Changes
| Change | Impact | Compatibility |
|---|---|---|
| Add new endpoint | None | Yes |
| Add new response field | None | Yes |
| Add optional field | None | Yes |
| Remove response field | Breaking | No |
| Rename field | Breaking | No |
| Change field type | Breaking | No |
| Add required field | Breaking | No |
| Change error code meaning | Breaking | No |
| Change default behavior | Breaking | No |
Rule: Adding is safe, removing/changing is dangerous.
1.3 The Cruelty of Hyrum's Law
"With a sufficient number of users of an API, any observable behavior will be depended on by somebody."
Real examples:
- Clients that assume response field ordering
- Clients that parse exact error message text
- Clients that depend on side effects (e.g., sequential IDs)
- Assumptions about consistent response time
Conclusion: "It's fine to change what's not in the official docs" is wrong. Every observable behavior is part of the API.
2. Four Versioning Strategies
2.1 URL Path Versioning
GET /v1/users/123
GET /v2/users/123
Pros:
- Clearest
- Easy to debug (version visible from URL)
- Cache-friendly (different URLs = different caches)
Cons:
- New URL required for each major change
- Each version maintained as a separate codebase
- Clients hardcode the URL
Adopters: GitHub (/v3/), Twitter, Stripe (URL is /v1/ but actual version via header)
2.2 Header Versioning
GET /users/123 HTTP/1.1
Stripe-Version: 2024-04-15
Pros:
- Clean URLs
- Same resource, different representation
- Easy incremental migration
Cons:
- Hard to debug (if headers are hidden)
- Complex cache handling (Vary header)
- Client libraries must add headers automatically
Adopters: Stripe (most famous), Azure
2.3 Content Negotiation
GET /users/123 HTTP/1.1
Accept: application/vnd.example.user.v2+json
Pros:
- HTTP standard (uses Accept header)
- Different versions via same URL
- Fits well with HATEOAS
Cons:
- Complex
- Client must know the exact MIME type
Adopters: GitHub (optional, Accept: application/vnd.github.v3+json)
2.4 Query Parameter
GET /users/123?version=2
GET /users/123?api_version=2024-04-15
Pros:
- Simplest
- Visible in URL (easy to debug)
Cons:
- Looks like "part of the data" (though it's metadata)
- Complex cache handling
Adopters: Some simple APIs
2.5 Comparison Table
| Approach | Clean URL | Debugging | Cache | Adopters |
|---|---|---|---|---|
| URL Path | No | Excellent | Excellent | GitHub, Twitter |
| Header | Excellent | Poor | Good | Stripe, Azure |
| Content Negotiation | Excellent | OK | Excellent | GitHub (optional) |
| Query Param | OK | Excellent | Good | Simple APIs |
3. Stripe's Ingenious Date-Based Versioning
3.1 Core Idea
Stripe expresses versions as dates:
2024-04-15(API behavior on that date)2023-10-16(older behavior)2020-08-27(5-year-old behavior)
Every change is identified by a date.
3.2 Client Usage
import stripe
# Use account default version
stripe.api_version = "2024-04-15"
# Or per-request
stripe.Charge.create(
amount=2000,
currency="usd",
api_version="2023-10-16" # Old behavior
)
3.3 Stripe's Secret — the Transformation Layer
The server has exactly one codebase (the latest version).
For each request:
- Check client version
- Transform request to latest version (forward transform)
- Process
- Transform response to client version (backward transform)
Client (v2020) → [transform] → latest code → [transform] → Client (v2020)
Effects:
- 5-year-old clients still work
- Code stays current
- New features reach all users immediately (opt-in)
3.4 Transformation Example
v2020 to v2024 change: added description field to response.
# Transformation function
def transform_to_v2020(response):
if "description" in response:
del response["description"] # v2020 clients don't know this field
return response
v2020 to v2024 change: amount changed from integer to object.
def transform_to_v2020(response):
if isinstance(response.get("amount"), dict):
response["amount"] = response["amount"]["value"]
return response
3.5 Stripe's Changelog
Each version change is documented precisely:
2024-04-15
- Added
descriptionfield toChargeobjectamountfield type changed from integer to AmountObject- Default
currencyis now derived from account settingsMigration guide: ...
This level of transparency is the core of trust.
4. GraphQL's Versionless Evolution
4.1 Core Philosophy
GraphQL doesn't use versions. Instead, it evolves field by field:
- Add: add a new field — existing clients don't know about it, so no impact
- Remove: mark with
@deprecated, then remove after some time
type User {
id: ID!
name: String!
# Deprecated field
email: String @deprecated(reason: "Use 'emailAddress' instead. Will be removed 2025-12-31")
emailAddress: String!
}
4.2 Clients Request Exactly What They Need
query {
user(id: "123") {
id
name
emailAddress # Request only the new field
}
}
Existing clients keep requesting email and continue working. New clients use emailAddress.
No over-fetching = adding new fields is free.
4.3 Deprecation Tracking
The GraphQL server collects usage statistics:
- Which clients are using
email? - When was the last use?
- Can it be safely removed?
Apollo Studio and Hasura Cloud provide this functionality.
4.4 GraphQL's Limits
- Not every change is compatible — type changes, enum value removal, etc., are still breaking
- Client code generation — needs to be rebuilt with new schema
- Advanced tooling required — usage tracking, etc.
Adopters: GitHub, Shopify, Twitter, Airbnb
5. Semantic Versioning and APIs
5.1 SemVer Basics
MAJOR.MINOR.PATCH
v1.2.3
- MAJOR: compatibility broken (breaking)
- MINOR: compatible + new features
- PATCH: compatible + bug fixes
5.2 Library vs API
Library: SemVer fits naturally.
npm install foo@^1.0.0→ auto-updates 1.x.x
Web API: hard to apply.
- Clients cannot auto-update
- The difference between "v1.2" and "v1.3" is meaningful, but "v1.2.3" vs "v1.2.4" is nearly meaningless
Reality: Web APIs usually expose only major versions (/v1, /v2).
5.3 SemVer's Limits
When v2.0.0 ships, every user must migrate. Incremental evolution is hard.
→ Stripe's approach (date-based) or GraphQL's approach (versionless) is more elegant.
6. Deprecation and Sunset
6.1 Deprecation Stages
- Announce: blog, email, changelog
- Mark in API: response header or field
- Monitor: track usage
- Reminder: notify users directly
- Sunset: retire (HTTP 410 Gone)
6.2 Deprecation Headers
RFC 8594: the HTTP Sunset header
HTTP/1.1 200 OK
Sunset: Sat, 31 Dec 2025 23:59:59 GMT
Deprecation: Sat, 31 Dec 2024 23:59:59 GMT
Link: <https://api.example.com/docs/migration>; rel="deprecation"
Meaning:
Deprecation: already deprecated (still working)Sunset: retirement scheduleLink: migration guide
6.3 Deprecation Messages (Response Body)
{
"data": {...},
"warnings": [
{
"code": "DEPRECATED_FIELD",
"message": "Field 'email' is deprecated. Use 'emailAddress' instead.",
"documentation_url": "https://api.example.com/docs/v2#email-deprecation",
"sunset_date": "2025-12-31"
}
]
}
6.4 Notifying Users
Technical:
- Response headers (
Sunset,Deprecation) - Warnings field in the response body
Communication:
- Email (registered developers)
- Blog / changelog
- Dashboard notice
- Direct contact (large users)
Stripe: sends automatic email about deprecated APIs in use.
6.5 Sunset Policy Example
| User | Sunset Period |
|---|---|
| Free users | 6 months |
| Paid users | 1 year |
| Enterprise | 2 years |
Larger companies need more time to adapt.
7. Strategies to Avoid Breaking Changes
7.1 Prefer Additive Changes
Wrong change:
- "user_email"
+ "email"
Right change:
+ "email" // add new field
"user_email" // keep old field (deprecated)
Return both fields together. Gives clients time to migrate.
7.2 New Endpoint vs Changing Existing
Bad: change the response shape of existing /users.
Good: new /v2/users endpoint, or /users?format=new.
7.3 Be Careful with Defaults
// v1
{ "page_size": 20 } // default 20
// v2 — change to 50?
{ "page_size": 50 } // Breaking! (changes pagination behavior)
Default-value changes are often breaking.
7.4 Optional → Required Is Breaking
- email: string? // optional
+ email: string // required
If existing clients don't send email, they fail. Don't add it.
7.5 Is Adding Enum Values Safe?
enum Status {
ACTIVE,
INACTIVE,
+ PENDING_REVIEW // new value
}
Subtle: if the client handles the enum with a switch, a default case is required for the new value. If present, safe; if not, subtle bug.
Advice: adding enum values is technically backward compatible, but requires client code review.
8. Real-World API Evolution Case Studies
8.1 Stripe — the Elegance of Date-Based
- 10+ years of API evolution
- Precise date for every change
- Clients upgrade at their own pace
- Usage stats + automated notifications
8.2 GitHub — REST to GraphQL
- REST v3: in operation since 2014
- GraphQL v4: launched 2017
- Two APIs run in parallel
- New features go to GraphQL first
Lesson: leave the old API alone, start a new paradigm separately.
8.3 Twilio — Major Version + Gradual Migration
- Date prefixes like
/2008-08-01/,/2010-04-01/ - But major changes get new prefixes
- Old versions kept for years
8.4 Slack — Gradual Deprecation
- Frequent new methods added
- Deprecation announced 6 months to 1 year in advance
- Direct emails to users
8.5 AWS — Almost Never Breaks
- The S3 API launched in 2006 still works
- New features are added only; existing features never change
- Result: the API is inconsistent and complex, but compatibility is perfect
9. Best Practice Checklist
9.1 Design Phase
- Choose versioning strategy (URL/Header/date)
- Explicit SLA (how many years of support?)
- Document deprecation policy
- Automate changelog
9.2 On Change
- Is it a breaking change? (verify with checklist)
- Can it be made additive?
- Write a migration guide
- Add Sunset header
- Notify users
- Monitor usage statistics
9.3 At Sunset
- Wait until zero users
- Final notification
- Respond with HTTP 410 Gone
- Remove code (after more time)
10. The Future of API Evolution
10.1 OpenAPI 3.1 + JSON Schema
Automatic compatibility checks via schema:
- Detect breaking changes automatically on API spec change
- Auto-generate client code
10.2 AI-Based Migration
- AI analyzes code → auto-generates migration PRs
- Automated impact analysis of changes
10.3 Standardizing Contract Testing
- Tools like Pact and Spring Cloud Contract
- Enforce contracts between API providers and consumers
- Verify compatibility in CI
Quiz
1. What is the most common breaking change?
Answer: removing or renaming a response field. If clients are using that field, they break immediately. Safe alternative: add a new field and mark the old one deprecated (return both). Remove after a period. Other common breaking changes: field type changes (string to object), adding required fields, changing defaults, changing the meaning of enum values.
2. What are the advantages of Stripe's date-based versioning?
Answer: (1) Incremental migration — clients adopt new versions at their own pace, (2) Single codebase — the server maintains only the latest version and supports old clients via a transformation layer, (3) Clear changelog — the changes on each date are documented precisely, (4) Easy to test — you can explicitly test a specific-date version. The downside is that implementing the transformation layer is complex.
3. Why doesn't GraphQL use versions?
Answer: In GraphQL, clients request exactly the fields they need, so adding a new field has no impact on existing clients — they don't request it. Field removal is marked with @deprecated, and usage statistics are tracked so removal is safe. Result: an API that evolves forever without versions. Downside: not every change is compatible (type changes, enum value removal, etc.).
4. What is the role of the Sunset header?
Answer: an RFC 8594 standard HTTP header that tells clients "when this resource will be retired". Sunset: Sat, 31 Dec 2025 23:59:59 GMT. Clients can see this header and automatically recognize the migration schedule. Used together with Deprecation and Link headers (migration guide). It lets automated clients respond safely to the retirement schedule.
5. What does Hyrum's Law mean for API design?
Answer: "With a sufficient number of users of an API, any observable behavior will be depended on by somebody." In other words, you can't change things even if they aren't in the official documentation. Response field ordering, exact error message text, response time, ID sequentiality — all become "part of the API". Conclusions: (1) design carefully from day one, (2) consider every observable behavior when changing, (3) explicitly document unintended behavior as "do not depend on this behavior".
References
- Stripe API Versioning — explains the date-based approach
- GitHub API v3 to v4 — GraphQL migration
- RFC 8594 — Sunset Header
- Hyrum's Law
- OpenAPI 3.1 — API specification standard
- Pact — Contract testing
- GraphQL Best Practices
- API Stylebook — collection of per-company API guides
- Twilio API Versioning
- Building Evolvable Web APIs — Glenn Block et al.
- API Change Management at Stripe