Chapter 15 — Isolate DSM Details with a Domain Adapter
Chapter objective: After this chapter, a developer can wrap the three typed DSM handles in a domain port and keep locator, lifecycle, and failure translation inside the adapter boundary.
Learning objectives
- Identify the maintenance cost of DSM APIs leaking into business code.
- Design a
FulfillmentCoordinationport in business language. - Centralize Register, Lease, CRDT handles and locators in a composition root.
- Validate domain decisions and real Runtime behavior separately.
Prerequisites
- Collection selection from Chapter 9 is complete.
- Failure and business-commitment boundaries from Chapter 14 are understood.
- The Maven example in
examples/order-fulfillment-control-planecan run.
Case progress
Direct collection calls in the first three arcs exposed semantics. In
an application, the fulfillment use cases say "publish a route," "claim
a shard," and "record a request." They do not refer to
shared/gateway/route-hints or
LeaseAcquireOptions.
The domain port owns language; the adapter owns mechanism
The book example defines a small port:
public interface FulfillmentCoordination {
void publishRoute(String serviceKey, String address);
Optional<String> routeFor(String serviceKey);
ShardClaim claimShard(String shardId);
long incrementRequestCount();
}The port returns no DsmRegister,
LeaseSnapshot, PnCounterState, or
CollectionLocator. A fake can test the business service
directly. The DSM adapter translates the Lease result into
ShardClaim(granted, uncertain, fencingToken, reason).
One composition root
DsmFulfillmentModule owns:
- creation, start, and close of
DsmRuntime; clusterIdandserviceId;- three stable locators and schema IDs;
- entity, codec, and spec definitions for Register, Lease, and CRDT;
- mapping of three typed handles into the domain port.
Locator and schema remain important contracts. The adapter owns them so they do not spread through controllers, services, and jobs.
Failure translation preserves uncertainty
A Lease acquire result contains granted,
uncertain, a snapshot, and a reason. Reducing it to a
boolean prevents the caller from distinguishing explicit rejection from
an uncertain result. The domain result retains information required by a
business decision while preventing arbitrary handle access.
The fencing token also crosses the port into a protected downstream write. Hiding it would break the Chapter 7 boundary. The example returns the token but does not implement a real downstream guard, so the acceptance card does not claim external fencing.
Two test classes prove two layers
RecordingCoordinationshows thatFulfillmentServicedepends only on the domain port: E1.- A real
DsmRuntimeand three collection types validate the adapter in one process: E2.
Only the second makes business decisions and DSM assembly inseparable; only the first can stay green while a real spec, codec, or Lease fails. Both are needed.
Counterexample and fault injection
- Copy locator strings into
FulfillmentService, change a collection ID, and count business files touched. - Reduce
LeaseAcquireResultto a boolean and try to expressuncertain=true. - Omit
runtime.start()and locate the lifecycle failure. - Let business code call
requestCounter.state()and observe CRDT types in domain tests.
Experiment
cd examples/order-fulfillment-control-plane
mvn clean testInspect port-contract.json. Each business action
identifies domain input/output, DSM handle, and external responsibility
outside the adapter.
Experiment acceptance card
| Field | Content |
|---|---|
| Command | cd examples/order-fulfillment-control-plane && mvn clean test |
| Input or fault | fake port, real single-node Runtime, and three typed handles |
| Observable result | domain service imports no DSM types; adapter performs route, Lease, and counter operations |
| Evidence level | E1 domain unit test plus E2 single-process Runtime behavior |
| This experiment does not prove | Multi-node propagation, repair, real network behavior, downstream fencing, or production assembly |
Review
- Domain ports own business language; adapters own DSM mechanism.
- Locator, schema, codec, and lifecycle belong in the composition root.
- Failure translation retains uncertainty and the fencing token.
- Fakes and real Runtime tests answer different questions.
Next
Chapter 16 replaces manual assembly with Spring Boot properties and conditional beans, then validates context startup and lifecycle.