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
nullfor 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
| Dimension | Strict Contract | Loose Contract |
|---|---|---|
| Coupling | Tight — consumer tied to full schema | Loose — consumer takes only what it needs |
| Type safety | High — schema validated end-to-end | Lower — unread fields are unvalidated |
| Evolvability | Low — adding fields is a breaking change | High — adding fields is non-breaking |
| Consumer coordination | Required on every producer change | Only required when a used field changes |
| Version management | Complex — many versions to maintain | Simpler — older consumers often continue working |
| Breaking change detection | Easy — schema mismatch is immediately visible | Harder — requires consumer-driven contract tests |
| Regulatory compliance | Better — full schema validation at boundary | Worse — unread fields bypass validation |
| Best for | Financial, legal, high-trust small teams | Large 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)
- 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.
- Pact is published to a shared broker (the Pact Broker).
- 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.
- 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
- Unnecessary bandwidth: Serializing and deserializing 15 fields when only 2 are needed wastes network bandwidth and CPU — especially at scale with high-frequency messages.
- 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
TicketFullintoTicketHeader+TicketDetail), the consumer must change even though the two fields it cares about haven’t moved. - 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.
- 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
| Context | Stamp Coupling Assessment |
|---|---|
| Service passes full object when only 2 fields needed | Problem — unnecessary coupling and bandwidth |
| Notification service receiving a full ticket object | Problem — use a targeted event instead |
| Workflow state carrier passed through an ordered pipeline | Legitimate — the coupling is intentional and serves a purpose |
| Cross-team workflow where each team owns a step | Borderline — evaluate whether a shared schema creates coordination burden |
| Security-sensitive data passed to services with no need to see it | Always a problem — violates least privilege |
Trade-off Summary
| Decision | Options | Key Trade-off |
|---|---|---|
| Contract strictness | Strict vs. loose | Type safety and validation vs. evolvability and consumer autonomy |
| Contract testing approach | Schema validation vs. CDCT | Catch all schema violations vs. catch only violations that affect real consumers |
| Versioning strategy | URI / Header / Semantic | Visibility and simplicity vs. URI stability and negotiation flexibility |
| Stamp coupling | Fat message vs. targeted message | Workflow simplicity vs. consumer coupling and bandwidth efficiency |
| Workflow state | Stamp coupling vs. central store query | Pipeline 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:
-
Notification coupling: The
NotificationServicewas initially receiving the fullTicketFullobject to know when to send alerts. The team recognizes this as stamp coupling and refactors to a targetedTicketStatusChangedEventwith onlyticketId,customerId,newStatus, andcontactPreference. -
Reporting service: The
ReportingServicelegitimately 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. -
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
TicketManagementteam confidence that schema changes won’t cause silent failures. -
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
- 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.
- Strict contracts provide type safety and validation but create tight coupling — every schema change may force all consumers to release a new version simultaneously.
- Loose contracts (Tolerant Reader pattern) allow producers to evolve their schema without breaking consumers, at the cost of reduced boundary validation.
- 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.
- 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.
- 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.
- Versioning strategies (URI, header, semantic) each make different trade-offs between visibility, URI stability, and formal break/non-break communication.
- 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.
- 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.
- 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.
Related Resources
- ch03-architectural-modularity — Service boundaries and cohesion that determine where contracts exist
- ch04-coupling — The coupling vocabulary that underlies the strict/loose distinction
- ch14-managing-analytical-data — Data contracts and schema evolution in the analytical data context
- ch12-transactional-sagas — Contracts in the context of distributed transactions and saga choreography
- ch15-build-your-own-tradeoff-analysis — The meta-framework for making contract decisions
Last Updated: 2026-05-30