FREE LESSON · Applications & software architecture · 5 OF 11

Module boundaries and domain invariants

Module boundaries and domain invariants: Derive an interface from ownership of a rule, then test the boundary.

Prerequisites: Functions, data structures, and invariants.

Learning objectives

  • Separate a representation from its public operations.
  • Trace a rule through callers and identify duplicated authority.
  • Evaluate a change using dependency direction and contract tests.

An interface controls the ways state can change

A module is a unit of reasoning when callers can use its contract without depending on its representation. Consider a library with one physical copy of a book. Exposing a mutable dictionary lets every caller write its holder field. A reservation module instead offers reserve(copy_id, reader_id), cancel(copy_id, reader_id), and lookup(copy_id). Its invariant is that a copy has at most one active holder. Its contract must also say who may cancel, what happens for a missing copy, and whether a retry returns the original result.

Encapsulation is useful only if the implementation actually controls all writes. A private field that can be modified through a leaked mutable reference is not protected. Likewise, validation in an HTTP controller cannot protect writes made by a background import unless the shared domain boundary enforces the same rule. List every state-changing path before asserting that the invariant holds.

Work a change-impact analysis

Assume three callers: a web route, a mobile route, and an import job. In design A, each calculates whether a reservation is allowed. A policy change requires three edits and three opportunities for disagreement. In design B, callers translate their inputs into the same reserve operation. The module changes once, while callers continue using the contract. This reduces duplicated policy, but does not remove the need to test each adapter's translation and authorization.

Now introduce multiple copies of a title. If callers depend on a storage row's holder column, all may need changes. If they request a reservation by title and receive an opaque reservation ID, the representation can evolve behind that contract. The latter interface is not universally better: a desk worker may need to select a specific damaged or accessible copy. A good boundary preserves distinctions required by real use rather than hiding everything indiscriminately.

Make dependency direction visible

Draw arrows from code that knows about another module to that module. A domain rule importing a particular HTTP framework and database client is harder to exercise independently. It can instead depend on small operations supplied by adapters. This is a dependency decision, not a requirement to create an interface for every class. Indirection has a cost in navigation, debugging, and vocabulary.

A useful test crosses the public boundary: reserve an available copy, try a second reader, cancel with the wrong reader, then retry the original request. Assert observable state and results. A test that mocks every internal call can preserve implementation details while missing the invariant. When you propose a module split, name one likely change it isolates and one operation it makes harder.

Practice problem

Design contracts for reserve and cancel. Give valid inputs, return cases, ownership checks, and the state after each of four calls: reserve(A), reserve(B), cancel(B), cancel(A). Then explain how the contract changes if reservations expire.

Worked solution

Initially the copy is free. reserve(A) creates an A-owned reservation. reserve(B) conflicts and leaves A in place. cancel(B) is denied and leaves A in place. cancel(A) frees the copy. Define whether cancelling an already-cancelled reservation is an idempotent success or an explicit absent result. With expiration, introduce a clock policy and reservation identity: a delayed cancellation for an expired reservation must not clear a later reservation. Test the expiration boundary and retries using an explicit clock supplied to the module.

Check your understanding

Which change most directly protects the invariant across every caller?

  • Copy validation into each controller.
  • Make the state-changing operation enforce the rule.
  • Hide the database filename.
  • Rename the holder field.
Explanation

The shared state-changing boundary must enforce the rule; caller-only validation misses other writers.

An expired reservation is replaced by a new reservation for the same reader. What should an old cancellation identify?

  • Only the reader.
  • Only the title.
  • The specific reservation being cancelled.
  • The current clock time only.
Explanation

Resource identity distinguishes two reservations by the same reader and prevents a stale cancellation from clearing the replacement.

Primary reading

Open the study and save your work →