Chapter 13: Contracts

saht contracts coupling microservices consumer-driven stamp-coupling

Status: Notes complete


Overview

Chapter 13 addresses one of the least glamorous but most consequential aspects of distributed architecture: how services communicate their expectations to each other. Every service-to-service interaction implies a contract — an agreement about what data will be sent, in what shape, and with what semantics. The chapter asks: how strict should that contract be, and who bears the burden when it changes?

The authors establish two poles: strict contracts, which demand exact compliance with a fully specified schema, and loose contracts, which allow consumers to take only what they need and tolerate additions they don’t understand. Both have legitimate uses, and neither is universally superior. The decision depends on the degree of control the teams have over both sides of the interface, the rate of schema evolution expected, and the cost of a breaking change in production.

The chapter also introduces stamp coupling — a deceptively common pattern in which a service passes far more data than the recipient needs, creating an implicit coupling that wastes bandwidth and makes schema evolution harder. It then distinguishes legitimate uses of stamp coupling (workflow state management) from problematic uses (lazy over-passing).

The Sysops Squad saga section shows these concepts in action as the team debates how to manage the ticketing domain’s contracts across the newly decomposed services.


Strict vs. Loose Contracts

Definitions

Strict contract: A consumer must conform exactly to the producer’s full published schema. Every field in the schema is significant; additional fields are rejected or cause errors; missing fields cause failures. The consumer’s code is written against the full contract definition.

Loose contract: A consumer extracts only the fields it needs and ignores everything else. The producer can add new fields without breaking existing consumers. The consumer’s code is insulated from schema growth.

The distinction is not merely technical — it is an organizational and operational stance on who bears the cost of change.

The Strict Contract Model

In a strict-contract model, both producer and consumer are tightly synchronized:

  • If the producer adds a field, consumers using strict parsing may reject the message as “unexpected”
  • If the producer removes or renames a field the consumer expects, the consumer breaks
  • Version bumps are visible, explicit, and must be coordinated across teams

When strict contracts make sense:

  • Financial, legal, or regulatory domains where every field is meaningful and unintended fields must be rejected
  • Small, co-located teams who own both the producer and consumer and can coordinate changes
  • High-trust, low-churn interfaces where the schema is unlikely to evolve
  • When type safety and validation at the boundary is a hard requirement (e.g., PCI-DSS message validation)

Risks of strict contracts:

  • A producer adding an innocuous optional field forces all consumers to release a new version
  • Version proliferation: multiple versions of the same API must be maintained simultaneously
  • Cross-team coordination overhead becomes a bottleneck as the number of consumers grows
  • Tight coupling to schema structure means even non-breaking changes require consumer acknowledgment

The Loose Contract Model

In a loose-contract model, consumers are written defensively:

  • Parse only the fields you need; ignore everything else
  • If the producer adds a field, existing consumers are unaffected — they simply don’t see the new field
  • If the producer removes a field the consumer does not use, the consumer is unaffected
  • Only the removal of a field the consumer depends on is a breaking change

When loose contracts make sense:

  • Large organizations with many teams consuming the same producer — strict coupling would create a coordination nightmare
  • APIs that evolve frequently with new optional fields being added regularly
  • Event-driven architectures where events are published to many consumers with different needs
  • Microservices environments where teams cannot coordinate releases
  • Consumer-driven contract testing environments where each consumer’s actual usage is tested independently

Risks of loose contracts:

  • Harder to validate at the boundary: the producer cannot know whether its schema changes will break consumers that depend on fields it removes
  • Reduced type safety: a consumer may silently receive null for a field it expected but the producer removed
  • Discovery is harder: no single schema defines what the interface is
  • Can mask breaking changes: a field removal may go undetected by the producer team until a consumer team reports production failures

Strict vs. Loose Contracts Trade-off Table

DimensionStrict ContractLoose Contract
CouplingTight — consumer tied to full schemaLoose — consumer takes only what it needs
Type safetyHigh — schema validated end-to-endLower — unread fields are unvalidated
EvolvabilityLow — adding fields is a breaking changeHigh — adding fields is non-breaking
Consumer coordinationRequired on every producer changeOnly required when a used field changes
Version managementComplex — many versions to maintainSimpler — older consumers often continue working
Breaking change detectionEasy — schema mismatch is immediately visibleHarder — requires consumer-driven contract tests
Regulatory complianceBetter — full schema validation at boundaryWorse — unread fields bypass validation
Best forFinancial, legal, high-trust small teamsLarge organizations, event-driven, high-churn APIs

The Tolerance Spectrum

The authors place contracts on a tolerance spectrum:

Strict                                                    Loose
  |--------------------------------------------------|
  |         |              |              |          |
Exact     Versioned    Consumer-     Tolerant     Amorphous
Schema    Schemas       Driven        Reader      (JSON blob)
  • Exact schema: Consumer must match producer exactly (XML Schema, strict Protobuf)
  • Versioned schemas: Multiple explicit versions maintained in parallel
  • Consumer-driven contracts: Each consumer specifies what it needs; producer tests against all consumer expectations (Pact framework)
  • Tolerant reader: Consumer ignores fields it doesn’t understand (Martin Fowler’s Tolerant Reader pattern)
  • Amorphous: No schema enforced (raw JSON passed as string) — maximum flexibility, minimum safety

Consumer-Driven Contract Testing

Consumer-driven contract testing (CDCT) is a technique that threads the needle between strict and loose contracts by making the consumer’s actual usage the authoritative definition of the contract.

How It Works (Pact Framework)

  1. Consumer writes a contract: The consumer team writes a test that records what fields it reads and what interactions it expects from the producer. Pact generates a contract file (a “pact”) from this test.
  2. Pact is published to a shared broker (the Pact Broker).
  3. Producer verifies against all pacts: In the producer’s CI pipeline, it runs a verification step that replays each consumer’s expected interactions against the real producer. If the producer would break a consumer’s contract, the build fails.
  4. Deployment gates: Neither the consumer nor the producer can deploy if they would break a verified contract.

Why CDCT Is Powerful

  • The producer knows exactly which fields any consumer actually uses — not which fields are in the schema
  • Removing an unused field is safe; removing a used field is caught in CI before production
  • Consumers are free to evolve independently; the pact records their real needs
  • Avoids the “big bang” version upgrade: each consumer upgrades when it’s ready, and the pact reflects the current state

CDCT Limitations

  • Requires discipline: all consumers must maintain up-to-date pacts
  • Works best in organizations where teams control both consumer and producer testing
  • Does not validate semantic correctness — only that the field exists and has the expected type
  • Adds CI/CD infrastructure complexity (Pact Broker, verification pipelines)

Versioning Strategies

When strict contracts are necessary, versioning is unavoidable. The authors cover three primary strategies:

URI Versioning

GET /api/v1/tickets/{id}
GET /api/v2/tickets/{id}
  • Pro: Explicit, cacheable, easy to route
  • Con: Consumers must explicitly upgrade; multiple versions must be maintained in parallel

Header Versioning

Accept: application/vnd.sysops.ticket.v2+json
  • Pro: URI remains stable; versioning is a negotiation between client and server
  • Con: Less visible, harder to cache, requires infrastructure support for content negotiation

Semantic Versioning of Schemas

  • Use major.minor.patch semantics for schema versions
  • Major: Breaking change (field removed, type changed, required field added)
  • Minor: Non-breaking addition (optional field added)
  • Patch: Documentation or metadata only
  • Pro: Encodes break/non-break distinction formally
  • Con: Requires consumer discipline to handle minor version upgrades gracefully

Stamp Coupling

What Is Stamp Coupling?

Stamp coupling (also called data-structure coupling) occurs when a component passes a large, composite data structure to another component even though the recipient only needs a small subset of the fields in that structure. The recipient is “stamped” with a dependency on the full structure, even though it only reads two fields.

Example:

TicketFull {
  ticketId, customerId, description, priority, status,
  createdAt, updatedAt, assignedTechnicianId, locationId,
  contractId, billingCode, attachments[], auditLog[]
}

If the NotificationService receives a TicketFull object but only reads ticketId and customerId, it is stamp-coupled to TicketFull. Any change to TicketFull’s schema potentially requires recompiling or redeploying NotificationService, even if the fields it uses are unchanged.

Why Stamp Coupling Is a Problem

  1. Unnecessary bandwidth: Serializing and deserializing 15 fields when only 2 are needed wastes network bandwidth and CPU — especially at scale with high-frequency messages.
  2. Artificial coupling: The consuming service is coupled to the producing service’s internal data model. When the producer refactors its data structure (e.g., splits TicketFull into TicketHeader + TicketDetail), the consumer must change even though the two fields it cares about haven’t moved.
  3. Versioning burden: Every schema change to the large object becomes a potential breaking change for every consumer, regardless of which fields those consumers actually use.
  4. Security/privacy concerns: Passing a large object may expose sensitive fields (e.g., billing codes, audit logs) to services that have no need to see them — violating the principle of least privilege.

How to Fix Stamp Coupling (The Non-Workflow Case)

Option 1: Field filtering — The producer provides a slimmer, purpose-built message for each consumer:

TicketNotificationEvent {
  ticketId, customerId   // Only what notification needs
}

Option 2: API composition — The consumer queries for only what it needs via a targeted API endpoint rather than receiving a fat event.

Option 3: Projection/view — The producer publishes multiple projections of the same underlying data, one per consumer type.

Stamp Coupling for Workflow Management (Legitimate Use)

The authors make an important distinction: stamp coupling is not always a problem. In workflow management scenarios, passing a large object between workflow steps is a deliberate design choice that serves a purpose.

The scenario: A multi-step workflow where each step processes the ticket and passes the enriched result to the next step. The full state of the workflow — including data added by previous steps — must travel with the message.

Step 1: CreateTicket  -->  TicketWorkflowMessage { ...initial fields... }
Step 2: AssignTech    -->  TicketWorkflowMessage { ...+ assignedTechnicianId... }
Step 3: Schedule      -->  TicketWorkflowMessage { ...+ appointmentTime... }
Step 4: Notify        -->  TicketWorkflowMessage { ...+ notificationSent... }

In this case, the large object is the workflow state carrier. Each step enriches it and passes it forward. The coupling is not accidental — it is the mechanism by which workflow state is shared without requiring each step to query a central database.

When stamp coupling for workflow is legitimate:

  • The steps form a well-defined, ordered pipeline
  • Each step genuinely adds information the next step needs
  • The alternative (each step querying a central database for context) would create worse coupling (to the database) and worse performance (multiple round-trips)
  • The workflow is owned by a single team that controls all steps

When it becomes a problem even in workflows:

  • The workflow object grows unboundedly over time (schema bloat)
  • Steps are owned by different teams who now share a coupling point
  • The object contains sensitive data that some steps should not see

Stamp Coupling Trade-off Summary

ContextStamp Coupling Assessment
Service passes full object when only 2 fields neededProblem — unnecessary coupling and bandwidth
Notification service receiving a full ticket objectProblem — use a targeted event instead
Workflow state carrier passed through an ordered pipelineLegitimate — the coupling is intentional and serves a purpose
Cross-team workflow where each team owns a stepBorderline — evaluate whether a shared schema creates coordination burden
Security-sensitive data passed to services with no need to see itAlways a problem — violates least privilege

Trade-off Summary

DecisionOptionsKey Trade-off
Contract strictnessStrict vs. looseType safety and validation vs. evolvability and consumer autonomy
Contract testing approachSchema validation vs. CDCTCatch all schema violations vs. catch only violations that affect real consumers
Versioning strategyURI / Header / SemanticVisibility and simplicity vs. URI stability and negotiation flexibility
Stamp couplingFat message vs. targeted messageWorkflow simplicity vs. consumer coupling and bandwidth efficiency
Workflow stateStamp coupling vs. central store queryPipeline simplicity vs. database coupling and latency

Decision Framework

Should you use strict or loose contracts?

  • Do both teams agree to coordinate on every schema change? → Strict
  • Does the consumer need field-level validation for regulatory reasons? → Strict
  • Are there many consumers with different needs? → Loose
  • Does the producer’s schema change frequently? → Loose
  • Can you implement consumer-driven contract testing? → Loose with CDCT as a safety net

Is your stamp coupling a problem or a feature?

  • Is the object being passed larger than what the recipient needs? → Yes → Evaluate
  • Is the recipient in a different team from the producer? → Yes → Likely a problem
  • Is the object being passed between steps in a single-team workflow pipeline? → Likely legitimate
  • Does the object contain fields the recipient should not see? → Always a problem

Which versioning strategy?

  • Do you need CDN caching and simple routing? → URI versioning
  • Do you want URI stability and serve multiple consumers simultaneously? → Header versioning
  • Do you want to communicate breaking vs. non-breaking formally? → Semantic versioning of schemas

Sysops Squad Saga: Managing Ticketing Contracts

In the Sysops Squad scenario, the decomposed architecture has multiple services that previously shared a monolithic database and now communicate via APIs and events. The saga in Chapter 13 focuses on how the team decides to manage the contracts between the TicketManagement, NotificationService, SurveyService, and ReportingService.

Key decisions the team navigates:

  1. Notification coupling: The NotificationService was initially receiving the full TicketFull object to know when to send alerts. The team recognizes this as stamp coupling and refactors to a targeted TicketStatusChangedEvent with only ticketId, customerId, newStatus, and contactPreference.

  2. Reporting service: The ReportingService legitimately needs many fields for analytics, and the team debates whether this is a valid use of stamp coupling or whether it should query a read model directly. They settle on a dedicated read model (CQRS-style) that the reporting service queries, avoiding stamp coupling via events.

  3. Contract testing: The team adopts consumer-driven contract testing via Pact for the internal APIs, allowing each service team to declare what it actually needs and giving the TicketManagement team confidence that schema changes won’t cause silent failures.

  4. Versioning: For the external customer-facing API, the team uses URI versioning (/v1/, /v2/) because the external consumers (customer portal, mobile apps) cannot be forced to upgrade on the same schedule as internal services.


Key Takeaways

  1. Contracts in distributed systems are not optional — every service interaction has an implicit or explicit contract, and the choice between strict and loose determines who bears the cost of change.
  2. Strict contracts provide type safety and validation but create tight coupling — every schema change may force all consumers to release a new version simultaneously.
  3. Loose contracts (Tolerant Reader pattern) allow producers to evolve their schema without breaking consumers, at the cost of reduced boundary validation.
  4. Consumer-driven contract testing (Pact) provides a middle path: each consumer specifies its actual usage as a test, and the producer’s CI pipeline verifies that no consumer’s contract is violated before deployment.
  5. Stamp coupling — passing a large object when only a few fields are needed — creates unnecessary bandwidth consumption, artificial schema coupling, and versioning burden for consumers.
  6. Stamp coupling is legitimate in workflow state management, where the large object serves as the carrier of accumulated workflow state passed between pipeline steps — but this legitimacy requires that all steps are controlled and that the object doesn’t grow unboundedly.
  7. Versioning strategies (URI, header, semantic) each make different trade-offs between visibility, URI stability, and formal break/non-break communication.
  8. The security dimension of stamp coupling is often overlooked: passing an object with sensitive fields to a service that doesn’t need them violates the principle of least privilege.
  9. The number of consumers is a key variable: with one or two consumers, strict contracts are manageable; with many consumers, loose contracts or CDCT become nearly mandatory.
  10. Contract decisions are architectural decisions — they are hard to change after the fact because they require coordinated changes across teams that may be on different release schedules.

Last Updated: 2026-05-30