Skip to content

PATTERN Cited by 1 source

Accept-header format negotiation for legacy sunset

Problem

When a service replaces a legacy interface, the consuming fleet doesn't migrate in a day. At Zalando's scale the pre-PRAPI Product pipeline had ~350 engineering teams consuming at least three distinct legacy data formats (alpha, beta, gamma) plus an intended new standard. A big-bang cutover is infeasible:

  • Too many consumers to coordinate.
  • Some consumers have slow release cadences.
  • Some consumers are downstream of other consumers (cutting over leaf consumers before their upstream is done is impossible).

The pattern the team used: one endpoint, many formats, differentiated by Accept header.

"Engineers meticulously analyzed and replicated existing transformations within PRAPI, allowing client teams to request data in their required format via the Accept header: application/json — New standard format for all teams; application/x.alpha-format+json — Legacy (previously on event stream); application/x.beta-format+json — Legacy (previously on event stream); application/x.gamma-format+json — Legacy (from Presentation API)."

(Source: sources/2025-03-06-zalando-from-event-driven-chaos-to-a-blazingly-fast-serving-api.)

Pattern

  • Expose a single URL per logical resource.
  • Accept every legacy format as a distinct media type via Accept header — usually with a vendor-prefixed name (application/x.<name>+json) so they can't collide with IANA-registered types.
  • Default (no Accept or */*) to the new canonical format.
  • Internally, store only the canonical form; serialise into the legacy formats on the response path.
  • Track usage per format. Every legacy format has a sunset window; usage tracking tells you who's still on it and when you can actually turn it off.

Why Accept headers over URL versioning

  • One URL — consumers don't have to change their configs to migrate to the new format; flipping a header is enough.
  • Per-request control — the same consumer can issue different requests with different Accept values, useful during a staged per-endpoint migration.
  • No URL churn — URLs stay stable; only the data shape varies.
  • Headers are easy to forward through proxies and gateways; URL rewriting in multi-hop paths is fragile.

Pairing with legacy-format emission

If the legacy format was previously delivered via an event stream (not just HTTP), Accept-header negotiation alone doesn't cover consumers that read from the stream. The complementary pattern is legacy-format emission for incremental sunset: have the new service re-emit the legacy formats onto the legacy streams for a fixed sunset window. Producers can decommission immediately; consumers migrate on their schedule.

Zalando ran both in parallel: HTTP consumers used Accept headers; event-stream consumers continued reading alpha/beta from the original streams, which PRAPI itself was now emitting.

Trade-offs

  • Transformation-maintenance cost. Every legacy format is a transformation the new service has to keep working and test. The investment is justified for migration but is debt; these transforms should be deleted at sunset.
  • Schema drift risk. If the canonical model loses a field that the legacy format carries, the transform has to reconstruct it — either by keeping the field in canonical form, deriving deterministically, or returning a default. Each option has failure modes.
  • Monitoring complexity. Per-format metrics must be tracked independently; p99 latency with alpha format may differ from the default if serialisation is heavier.

When this is wrong

  • Small consumer count with good coordination — just do a big-bang cutover.
  • Format differences reflect genuine semantic differencesAccept headers for behaviour-altering changes are confusing; URL versioning or explicit endpoint split may be clearer.
  • Legacy format is inherently incompatible. If the canonical model cannot reconstruct the legacy shape without loss, don't pretend — provide migration tooling instead.

Seen in

Last updated · 501 distilled / 1,218 read