API Versioning System Low-Level Design

Why API Versioning Matters

APIs are contracts between your service and its consumers. Once published, breaking changes — renaming fields, changing types, removing endpoints — break existing clients. API versioning lets you evolve your API without breaking existing integrations. Stripe’s API versioning is the gold standard: clients pin to a version; breaking changes are released as new versions; old versions are supported for years. Understanding the trade-offs between URL versioning, header versioning, and date-based versioning is a common system design topic.

Versioning Strategies

URL versioning — version in the path: /v1/users, /v2/users

GET /v1/users/123   → {"id": 123, "full_name": "John Doe"}
GET /v2/users/123   → {"id": "usr_123", "name": {"first": "John", "last": "Doe"}}

Pros: explicit, cacheable, easy to route. Cons: URL pollution; clients must update URLs when migrating.

Header versioning: Accept: application/vnd.myapi.v2+json or custom header X-API-Version: 2

Pros: clean URLs. Cons: harder to test in browser, not cached differently by CDN unless Vary header is set.

Date-based versioning (Stripe’s approach): Stripe-Version: 2023-10-16

# Client sends their pinned version date
Stripe-Version: 2023-10-16
# Server applies the schema/behavior for that date
# New customers default to the latest version; existing customers keep their pinned version

Pros: granular; clients see exactly what they integrated against. Cons: complex to maintain many date-versions.

Data Model for Version Management

ApiVersion(
    version     VARCHAR PRIMARY KEY,  -- 'v1', 'v2', '2023-10-16'
    status      ENUM(ACTIVE, DEPRECATED, SUNSET),
    released_at TIMESTAMPTZ,
    deprecated_at TIMESTAMPTZ,
    sunset_at   TIMESTAMPTZ,          -- date after which requests return 410 Gone
    changelog   TEXT
)

ClientApiVersion(
    client_id   UUID,
    api_key     VARCHAR,
    pinned_version VARCHAR,
    updated_at  TIMESTAMPTZ
)

Request Routing by Version

# Middleware: extract version from URL or header, attach to request context
@app.middleware
def version_middleware(request, call_next):
    # URL versioning: /v2/users → version='v2'
    version = extract_from_path(request.url.path)

    # Header versioning fallback
    if not version:
        version = request.headers.get('X-API-Version')

    # Default to latest stable version
    if not version:
        version = get_latest_stable_version()

    # Check sunset
    api_version = db.get(ApiVersion, version)
    if api_version.status == 'SUNSET':
        return Response(410, 'This API version has been sunset. Please upgrade.')

    # Deprecation warning header
    if api_version.status == 'DEPRECATED':
        request.state.deprecation_warning = (
            f'API version {version} is deprecated and will be sunset on '
            f'{api_version.sunset_at}. Please upgrade.'
        )

    request.state.api_version = version
    response = call_next(request)

    if hasattr(request.state, 'deprecation_warning'):
        response.headers['Deprecation'] = request.state.deprecation_warning
        response.headers['Sunset'] = api_version.sunset_at.isoformat()

    return response

Version-Specific Response Transformation

# Handler returns internal model; serializer applies version-specific transformation
def get_user(user_id, api_version):
    user = db.get_user(user_id)

    if api_version == 'v1':
        return {
            'id': user.id,          # integer in v1
            'full_name': user.name  # flat name in v1
        }
    elif api_version >= 'v2':
        return {
            'id': f'usr_{user.id}',    # prefixed string ID in v2
            'name': {                   # structured name in v2
                'first': user.first_name,
                'last': user.last_name
            },
            'created_at': user.created_at.isoformat()
        }

Deprecation and Migration Strategy

  1. Release v2 alongside v1 — both serve traffic simultaneously
  2. Announce deprecation of v1: add Deprecation and Sunset response headers
  3. Email clients using v1 with migration guide and sunset date (minimum 6-12 months)
  4. Monitor v1 traffic — track which client_ids still use the old version
  5. On sunset date: return 410 Gone with a message pointing to the migration guide
  6. After sunset: remove v1 code after another 3-6 months (keep routing to return 410)

Key Design Decisions

  • URL versioning for public APIs — explicit and easy to understand for third-party developers
  • Date-based for partner APIs — enables per-client pinned versions, fine-grained changelog
  • Sunset headers (RFC 8594) — machine-readable deprecation enables automated client alerts
  • Internal model separate from response schema — versioned serializers transform one internal model to multiple response shapes
  • Never break without versioning — backwards-compatible changes (adding fields) don’t require a new version

API versioning and backward compatibility design is discussed in Stripe system design interview questions.

API versioning and deprecation strategy is covered in Shopify system design interview preparation.

API versioning and public API design is discussed in Atlassian system design interview guide.

See also: Netflix Interview Guide 2026: Streaming Architecture, Recommendation Systems, and Engineering Excellence

See also: Scale AI Interview Guide 2026: Data Infrastructure, RLHF Pipelines, and ML Engineering

Scroll to Top