ADR · 07 · Data & Integration Contracts
Microservices promise independent deployability. Schema changes are the most common way
that promise breaks. The problem is not that schemas change — they must change as
systems mature. The problem is that changes without a compatibility contract mean
deployments that succeed individually and break things collectively.

What “Independent Deployability” Actually Requires

In a distributed system with 15 services and three consumer audiences, a schema change
in one producer service is a contract renegotiation with every consumer that reads it.
The number of implicit contracts in such a system is not 15 — it is the sum of every
field read by every consumer across every integration point.

The only way to maintain independence is to make the contracts explicit, versioned,
and tested before any deployment reaches production. This is not an ideal — it is the
mechanical precondition for the independence claim to be true.

Compatibility Modes — What Counts as Breaking

Not every schema change is equal. The distinction that matters in practice:

  • Safe by default: adding an optional field, adding a new event type,
    changing an internal field that no consumer reads.
  • Breaking: removing a field a consumer reads, renaming a field without
    a deprecation period, changing a field’s data type, adding a required field to an
    existing event.

Backward compatibility means existing consumers can read new events. Forward
compatibility means the current consumer can read events produced by older producers.
Full compatibility means both. For long-lived event streams — which in enterprise
systems means anything touching a financial record, an audit trail, or an SLA-governed
integration — full compatibility is the only safe target.

The Event Envelope

The Decision

Every domain event carries a standard envelope with a producer-assigned event version. Consumers check the version before applying a schema handler. Deprecated schema elements follow a four-step protocol: announce with a sunset date, notify affected consumers, monitor usage, remove only after confirmed zero consumption. No element is removed silently.

// Standard event envelope — every domain event
{
  "eventId":      "uuid-v4",
  "eventType":    "WorkOrderCreated",
  "eventVersion": "2.1.0",
  "occurredAt":   "2026-08-11T09:30:00Z",
  "correlationId":"uuid-v4",
  "schemaRef":    "schemas/work-order-created/v2",
  "payload": {
    // domain-specific content
  }
}

The eventVersion field is what makes multi-version consumers practical.
A consumer that supports v1 and v2 reads the version field and routes to the appropriate
handler. It does not need to guess from the payload shape. The producer does not need
to know which consumers are on which version — the envelope gives consumers the
information to decide for themselves.

MongoDB: Schema Discipline on a Flexible Platform

MongoDB’s schema-on-read flexibility is a design feature when used deliberately and a
liability when not. Collections without schema enforcement accumulate polymorphic
documents that become impossible to query reliably and expensive to migrate.

The discipline I apply: a schemaVersion field on every document type.
MongoDB’s JSON Schema validation enforced on write — required fields declared, types
constrained. Column additions (new optional fields) are always safe. Column removals
follow the same deprecation protocol as event schema changes. A background worker handles
migrations: reads documents at the old schema version, writes them at the new version,
both formats supported until migration is confirmed complete.

// MongoDB document schema discipline
{
  "_id":            ObjectId,
  "schemaVersion":  2,           // explicit — not inferred
  "documentType":   "WorkOrder", // discriminator for polymorphic collections
  "data": { ... },
  "createdAt":      ISODate,
  "migratedAt":     ISODate      // set when background worker upgrades this doc
}

Contract Tests: Where Governance Becomes Automation

Documentation tells you what the contract should be. Pact contract tests tell you
when it breaks — in CI, before either service reaches a shared environment.

Each consumer defines the fields it reads and their expected types. The producer’s
pipeline runs every consumer’s contracts before any deployment. A producer change that
removes a field a consumer reads fails the pipeline. The cost of catching it there is
a failed build. The cost of catching it in production is an incident, a rollback, and
a post-mortem. The tradeoff is not ambiguous.

Schema governance is not a process constraint. It is the engineering mechanism that makes “independent deployability” true rather than aspirational. Without explicit contracts and automated testing, independence is an assumption — one that breaks in proportion to the number of services and the speed of delivery.

Schema EvolutionMongoDBAzure Schema RegistryIBM MQIBM IIBEvent-Driven ArchitectureConsumer-Driven ContractsArchitecture Decision

← Back to Architecture Decisions