concept

Invariant Containment

also called Boundary Follows the Invariant, Transaction Test

The rule that a service boundary must contain any invariant that must hold exactly - so discovering that a split would break a transaction is evidence the boundary is wrong, not merely an obstacle.

temporalboundariestransactionssagaddd

When a proposed service split turns out to break a transaction, the usual response is to reach for a saga. The better first response is to treat the discovery as information: the two parts share an invariant, and shared invariants are the strongest available signal that a boundary is in the wrong place.

Boundaries drawn from a domain-modelling exercise frequently ignore invariants, which is why the collision appears at implementation time rather than at design time.

Why it matters

An invariant split across a boundary becomes either a distributed transaction — expensive, fragile, and usually unavailable — or a correctness bug that surfaces under concurrency. Neither is a good outcome, and both are avoidable by moving the boundary before anything is built.

Implementation patterns

The deciding question: is the invariant a data constraint or a business process?

  • A data constraint — a balance may not go negative, stock may not be oversold, an order may match at most once — must be enforced transactionally, and the boundary must contain it.
  • A business process — a booking with a payment and a notification — has a natural concept of partial completion and compensation, and can legitimately be a saga.

Confusing the two produces either an unnecessary distributed transaction or a correctness bug, and the distinction is a domain question that engineering cannot answer alone.

Then, in order of preference:

  • Move the boundary so the invariant is contained. Correct far more often than it is chosen.
  • Accept a saga, where the invariant is a process — trading atomicity for availability, with user-visible intermediate state as the currency of that trade, designed into the product rather than treated as an anomaly.
  • Use durable workflow execution for the orchestration when such workflows are numerous, long-running and expensive to get wrong.
  • Never split the code while sharing the database. All the deployment complexity of services and none of the independence, with the schema remaining a coupling point that makes both worse.

Industry example

Workflow engines such as Temporal exist for the legitimate saga case, and their adoption is frequently misapplied to the case where the boundary should simply have been drawn elsewhere. The distinguishing test is whether the business already has a concept of the partial state: a booking without a payment is a thing the business understands; an account balance that is temporarily wrong is not.

Failure scenarios

  • A distributed transaction across services, which is expensive and usually unavailable.
  • A saga applied to a data constraint, producing an invariant that holds eventually and therefore does not hold.
  • Code split with a shared database, the most common intermediate state and worse than either endpoint.
  • Intermediate states not modelled in the product, so users see a broken screen rather than a status.
  • The boundary defended because it came from a modelling exercise, despite the invariant evidence.

Trade-offs

Containing every invariant tends toward larger services, which reduces independent deployability — the property the split was for. In a domain with many interlocking invariants the containment argument can justify a monolith, and sometimes that is the correct conclusion.

The default when in doubt is not to split, because a boundary in the wrong place is expensive to move across a network and cheap to move inside a process. The cost of waiting is low and the cost of being wrong is high.

Interview question

"You are splitting checkout from inventory and discover the stock reservation and the order creation are in one transaction. Give me three options and tell me which you would choose and why."