SYSTEM_DESIGN
System Design: API Versioning Strategy
Design a robust API versioning system that manages multiple concurrent API versions, enables backward-compatible evolution, and provides seamless deprecation and migration paths for API consumers.
Requirements
Functional Requirements:
- Support multiple concurrent API versions simultaneously (v1, v2, v3)
- Route requests to the appropriate version handler based on version indicator
- Enforce version deprecation: warn consumers on deprecated versions, sunset after a date
- Transform requests/responses between versions when breaking changes are introduced
- Provide version-specific documentation and changelogs
- Allow consumers to pin to a specific version without breaking changes
Non-Functional Requirements:
- Version routing adds under 1ms overhead per request
- Support 50 active API versions simultaneously without operational complexity explosion
- Deprecation notices must reach all active consumers within 24 hours
- Zero-downtime version rollouts and removals
Scale Estimation
A large API platform: 1,000 external consumers, 10 API versions active simultaneously, 500,000 requests/sec total. Version distribution: v3 (latest) receives 70% of traffic, v2 receives 25%, v1 (deprecated) receives 5% (50,000 req/sec on deprecated version). Transformation logic (adapting v1 requests to internal format): runs on every v1 request, adding ~2ms per call. Storage for version metadata: 10 versions × 1,000 consumers × subscription info = trivially small.
High-Level Architecture
API versioning can be implemented at four levels: URL path versioning (/v1/resource), query parameter (?version=1), request header (Accept: application/vnd.api+json;version=1), or subdomain (v1.api.example.com). URL path versioning is the most common in practice due to its visibility, debuggability, and caching friendliness — version is visible in browser history, logs, and CDN cache keys without inspecting headers.
The API gateway serves as the version routing layer. Each incoming request's version is extracted from the URL path. The gateway maintains a version registry: a mapping from version string to backend handler cluster and transformation pipeline. Version handlers can be separate microservice deployments (v1 runs old code, v2 runs new code) or transformation adapters in front of a single backend (all versions use the same internal API, with the gateway translating between external versions and the internal format).
The transformation adapter approach (all versions on a single backend) reduces operational burden — only one backend to deploy and monitor. Request adapters transform v1 requests into the internal format; response adapters transform internal responses back to v1 format. These adapters are code-level transformations applied in the gateway's request pipeline. Version fan-out (deploying separate backends per version) is simpler conceptually but N× the operational overhead.
Core Components
Version Router
The version router parses the version from the request (URL path, header, or query param) and maps it to the appropriate handler configuration. The mapping is stored in the gateway's configuration as a version table: {"v1": {handler: "adapter-v1", deprecated: true, sunset_date: "2026-06-01"}, "v2": {handler: "backend", current: true}}. On each request, the router looks up the version, appends Deprecation and Sunset headers if applicable, and forwards to the handler. Unknown versions return 400 Bad Request; sunset versions (past sunset date) return 410 Gone.
Transformation Pipeline
For each deprecated version, a transformation pipeline converts between the old external API contract and the current internal API. Transformations are bidirectional: inbound (request adaptation) and outbound (response adaptation). Example: v1 expects a flat user object {name, email} but the internal API uses nested {profile: {name}, contact: {email}} — the inbound transformer expands v1 fields to the internal structure, the outbound transformer collapses the internal structure back to v1 format. Transformations are stateless functions applied synchronously in the gateway's request processing pipeline.
Deprecation & Sunset Lifecycle
When a new breaking version (v3) is released, v2 moves to DEPRECATED status. All v2 responses include: Deprecation: true, Sunset: Sat, 01 Jun 2026 00:00:00 GMT, Link: </v3/resource>; rel=successor-version. Consumer tracking: the gateway logs which consumer (by API key or OAuth client ID) is calling each version, enabling targeted outreach to consumers still on deprecated versions. After the sunset date, the gateway returns 410 Gone for that version, with a migration guide URL in the response body.
Database Design
PostgreSQL stores version metadata: api_versions (version, status: ACTIVE/DEPRECATED/SUNSET, release_date, sunset_date, changelog_url), consumer_version_usage (consumer_id, version, last_seen, request_count_24h). The consumer_version_usage table is populated asynchronously from gateway access logs — a stream processor aggregates version usage per consumer and upserts to PostgreSQL every 5 minutes. This data drives deprecation dashboards and consumer migration prioritization.
Version configuration (routing rules, transformation configs) is stored in the gateway's configuration store (etcd or Consul) for fast access without database queries on the hot request path.
API Design
Scaling & Bottlenecks
The transformation pipeline adds CPU overhead for deprecated version traffic. At 50,000 v1 requests/sec with 2ms transformation time, 100 CPU cores are needed solely for transformation — a strong incentive to aggressively migrate consumers off deprecated versions. Transformation code must be optimized (avoid reflection-based JSON transformation; use compiled schema transformations with code generation).
Maintaining parallel transformation code for 10 versions creates a testing and maintenance burden. After sunset, delete the transformation code and its tests entirely — this is why enforcing sunset dates is operationally important, not just a courtesy to consumers.
Key Trade-offs
- URL path vs. header versioning: URL path versioning is visible, cacheable, and debuggable; header versioning (Accept header) is more RESTful and keeps URLs clean but is harder to test in browsers and CDN-cache correctly
- Separate backends vs. transformation adapters: Separate backends allow independent rollback per version but multiply operational overhead; adapters centralize complexity in transformation code but allow a single deployment
- Breaking vs. non-breaking changes: A discipline of only introducing breaking changes in new major versions (and evolving non-breaking within a version) minimizes version proliferation — fields can be added, but removing/renaming fields always requires a new version
- Consumer pinning vs. auto-upgrade: Allowing consumers to pin to a specific version gives them stability but delays their migration; auto-upgrade (move consumers to latest within a major version) reduces maintenance burden but risks breaking consumers who relied on undocumented behavior
GO DEEPER
Master this topic in our 12-week cohort
Our Advanced System Design cohort covers this and 11 other deep-dive topics with live sessions, assignments, and expert feedback.