Skip to main content

Boundaries & contracts

A boundary defines who must coordinate when something changes. Good boundaries enable parallel teams; bad ones look like “one-line change, ten-repo PR” or “only staging catches breakage.”

Boundary types

TypeCarrierContractTypical failure
In-process modulePackage, target, namespacePublic API, no deep importsCycles
Inter-processHTTP/gRPCOpenAPI/Proto + versioningImplicit fields, breaking changes
AsyncMessages, eventsSchema registry, idempotency keysDuplicates, reordering
DataDB, cacheMigrations, read/write rulesShared-table writes

Design rules

  1. Slice by capability, not technology: “Billing”, “Notifications” beat mega utils.
  2. Contract over implementation: Promise schema and semantics; refactor inside freely.
  3. Anti-corruption layer (ACL): Translate third-party/legacy models at the edge; keep external types out of domain.
  4. Failure isolation: Timeouts, circuit breakers, bulkheads; document defaults and degradation.
warning

Distributed monolith: many deployables, one database, cross-writes—looks like microservices, behaves coupled. Prefer owned storage per service and collaboration via APIs/events.

API versioning

StrategyWhenPractice
Backward-compatible extensionPublic REST/JSONOptional fields only; stable semantics
Explicit versionLong-lived clients/v1 or header; sunset dates
Event versioningStreamstype + schemaVersion; dual-read window

Breaking changes (major bump or coordinated release):

  • Remove/rename fields, change types
  • Change error semantics or auth
  • Shrink allowed enum values

Context mapping (DDD cheat sheet)

RelationMeaningIntegration
Shared kernelShared model subsetShared lib (keep small)
Customer-supplierUpstream sets APISLA + contract tests
ACLIncompatible modelsTranslate at boundary
Open hostGeneric subdomainDocumented public API
Separate waysNo integrationAvoid fake coupling

Contract tests in delivery

RoleResponsibility
ProviderPublish contract artifacts (Pact, OpenAPI snapshots)
ConsumerCI verifies compatibility; failures block merge
EventsConsumers test sample payloads

Wire into CI/CD: contract failure = compile failure.

Review prompts

  • Does a new dependency import another module’s “internal” package?
  • Are timeout, retry, and idempotency symmetric across the boundary?
  • Stable error model (machine code + human message)?
  • Any “temporary” shared DTO used by more than two modules?