Skip to main content

Deployment Independence: Versioning and Backward Compatibility

What This Concept Is

Independent deployability (concept 01) is not a property of pipelines; it is a property of contracts. You can have separate CI/CD for every service and still be stuck in lockstep if any API change breaks a consumer.

Deployment independence means all of the following hold:

  • A producer can deploy a new version without any consumer deploying simultaneously.
  • During the transition, old and new consumers can coexist, each working with either old or new producers.
  • Breaking changes are never rolled out in place; they are rolled out as a new version coexisting with the old, with a deprecation window.

The rule of thumb: at any moment in production, any two adjacent versions of a producer must work with any live consumer version.

Why It Matters Here

Without this property, microservices impose all the costs of distribution and deliver none of the benefits. The shortest path to losing deployment independence is treating contract changes casually -- one renamed field, and three teams have to coordinate a release window.

Compatibility Types

KindDefinitionExample
Backward compatibleNew version works with old consumersAdd optional field, add new endpoint
Forward compatibleOld version works with new consumers (consumer ignores unknown fields)Tolerant reader, unknown enum handling
BreakingEither direction failsRename or remove field, change type, tighten validation, change URL

The target is both backward and forward compatible for every change. When that is impossible, version and coexist.

Safe and Unsafe Changes

Safe (ship freely):

  • Add a new endpoint.
  • Add a new optional request field (producer must default it).
  • Add a new response field (consumers using tolerant readers ignore it).
  • Relax a validation (accept more inputs).
  • Add a new event type.
  • Add a new optional event field.

Unsafe (requires versioning or migration):

  • Remove a field, endpoint, or event.
  • Rename a field.
  • Change a field's type (e.g., string -> int).
  • Tighten a validation (reject inputs the old version accepted).
  • Change the meaning of a field (semantic breaks are the worst kind -- the shape passes tests, but consumers misbehave).
  • Add a required request field without a safe default.

Versioning Strategies

For sync APIs:

  • URI versioning (/v1/orders, /v2/orders). Loud, clear, easy to route at the gateway. Default in REST.
  • Header versioning (Accept: application/vnd.orders.v2+json). Cleaner URLs, harder to discover, ignored by caches if misconfigured.
  • No versioning with strictly additive evolution. Works only when the whole org has strong tolerant-reader discipline.

For events:

  • Include an event_version field in every event.
  • When the new version is not backward compatible, publish both the old and new event types for a deprecation window, marking the old one deprecated.
  • Consumers migrate at their own pace during the window. After the window closes (with all consumers off the old type -- verified in the broker or with CDC), retire the old event.

Expand-Contract (Parallel Change) Pattern

The canonical way to make a "breaking" change without breaking anyone:

  1. Expand. Add the new thing alongside the old. Both endpoints/fields/events exist.
  2. Migrate consumers. One by one, consumers move to the new version. Use CDC tests to know when everyone has moved.
  3. Contract. Remove the old thing, once no consumer is using it.

For example, renaming a field user_id -> customer_id in a response:

  • v2.0: response contains both user_id and customer_id (same value). Safe.
  • v2.1..v2.9: consumers switch to reading customer_id. Each consumer's pact now expects customer_id.
  • v3.0: user_id is removed.

At no point is any consumer broken.

Concrete Example: A Safe Rollout

Orders service adds the partially_shipped status value. Consumers currently handle pending, confirmed, cancelled, shipped.

  1. Pre-rollout. Update consumers with tolerant enum handling: unknown values are treated as a safe default (e.g., "unknown", don't crash).
  2. Verify with CDC. Every consumer's pact test now tolerates unknown enum values. CDC passes.
  3. Producer rollout. Orders begins emitting partially_shipped where appropriate. No consumer crashes.
  4. Consumer upgrade (as needed). Consumers that actually benefit from distinguishing partially_shipped update their logic and ship independently.

Deployment Mechanics That Support Independence

  • Blue/green or canary deploys. New version lives alongside old; traffic shifts gradually; easy rollback.
  • Feature flags. Ship code inactive; enable per-cohort; decouple deploy from release.
  • Schema migrations with expand-contract for databases, mirroring the pattern above.
  • Independent pipelines per service. A red build in one service never blocks another.

Common Confusion / Misconception

"We just add versioning and we are fine." Versioning without tolerant readers and without CDC is still fragile. The three work together.

"Old versions are someone else's problem." No. The producer team owns both the old and new versions through the deprecation window. If that feels expensive, deprecation windows are too long; shorten them, but do not eliminate the overlap.

"Backward compatibility means never deleting anything." Deletion is fine -- it just happens in the "contract" phase of expand-contract, after the consumers have migrated.

How To Use It

  1. For every contract change, classify it as safe or unsafe.
  2. For unsafe changes, use expand-contract. Write down the three phases and the success criteria for moving between them.
  3. Use CDC tests (concept 09) to know when all consumers have migrated.
  4. Set a default deprecation window (e.g., 2 sprints) and stick to it.
  5. Prefer ship-often, small changes. Big bangs invite lockstep deploys by accident.

Check Yourself

  1. Why is independent deployability a property of contracts, not of pipelines?
  2. Explain expand-contract in terms a non-technical product manager would accept.
  3. What is the difference between backward compatibility and forward compatibility, and which one relies on tolerant readers?

Mini Drill or Application

Take a sync contract from concept 08. In 15 minutes, write the expand-contract plan for a field rename:

  • Phase 1 (expand): what the response looks like.
  • Phase 2 (migrate): how you know all consumers have moved.
  • Phase 3 (contract): the removal step and its rollback plan.

How This Sits In The Module

This is where the previous 13 concepts pay off: bounded contexts gave you clear edges, data ownership gave you safe schemas, contracts and CDC gave you enforceable shapes, and now versioning + expand-contract lets those edges move without anyone tripping.

Database Expand-Contract

The same pattern applies to database schema migrations, where the stakes are higher (data is persistent). For a column rename users.email_addr -> users.email:

  1. Expand. Migration adds the new column email. Application code writes to both columns (dual-write). Reads continue from email_addr (the authoritative column). No behavior change.
  2. Backfill. Batch job copies email_addr -> email for all existing rows. Dual-write continues.
  3. Switch reads. Application starts reading from email. Dual-write continues for safety.
  4. Stop writing the old column. Migration removes the write to email_addr. Schema still has both columns; the old one is now stale but harmless.
  5. Verify no reader remains. Use query logs or a "deprecated column" annotation to confirm.
  6. Contract. Drop email_addr in a final migration.

Each step deploys independently. Rollback is safe at every point. This maps directly onto the expand-contract pattern for APIs above; see also Pramod Sadalage and Scott Ambler's Refactoring Databases for the full catalog.

Continuous Delivery Preconditions

Deployment independence relies on a working continuous delivery pipeline (Jez Humble, David Farley, Continuous Delivery, 2010). Concretely:

  • Trunk-based development or short-lived feature branches (no long-running release branches).
  • Every commit is a release candidate. No "this commit is QA, this commit is prod" distinction.
  • Automated tests at each tier (unit, contract, component, a small number of end-to-end).
  • Deploy automation that is idempotent, observable, and fast (< 15 min from merge to prod for typical services).
  • Feature flags to decouple deploy from release (see Pete Hodgson's Feature Toggles).

Without these, versioning discipline alone does not deliver deployment independence; the pipeline becomes the bottleneck even if the contract is clean.

Read This Only If Stuck

Local chunks

External canonical references

Depth Path

  • Jez Humble and David Farley, Continuous Delivery -- the underlying engineering practices. Return if your pipelines are still slow or flaky.
  • Pramod Sadalage and Scott Ambler, Refactoring Databases -- the full catalog of expand-contract-style schema refactorings.
  • Dave Farley, Modern Software Engineering (2021) -- the updated version of the CD argument, with microservices-era examples.