Overview
Inside every module of the modulith, domain/ is the hexagon itself, split in two:
domain/contract, the surface API — ports in both directions, command and query
records, domain events, result and error types — and domain/core, the
implementation — entities, value objects, handlers, domain services. Below both sits
platform/kernel, the repository-wide dispatch vocabulary (Command, Query,
Handler, Mediator) that depends on nothing. The split is what lets adapters and
infrastructure compile against a module's contract while only the assembly ever
touches its implementation.
Key points
platform/kerneldepends on nothing — JDK only — and holds no business vocabulary: theCommand/Querymarker types, theHandlerinterface, theMediatorport and its registry implementation. Every module's contract depends on it; it depends on no module.domain/contractdepends only onplatform/kernel. No framework, no annotation, no logging facade, and — the modulith's addition — no other module. It is the most stable artifact in its context.- Both port directions live in
domain/contract: driving ports (what adapters call) and driven ports (what infrastructure implements). Driven ports must sit in the contract module, otherwise infrastructure would needdomain/coreto compile and the layering collapses. - A driven port is named after the need, never the provider. This is what keeps a
cross-module dependency out of the domain: billing declares
OrderLookup, notOrderingService, and never learns which module — or which process — answers it. See module composition. - Dispatch is Command/Query + Mediator, scoped per module (settled 2026-08-09,
re-scoped 2026-08-14). Each module has its own mediator over its own
handlers; sealed
Command/Queryrecords indomain/contractname the operations that module supports, handlers indomain/coreself-declare viasupports(), and the registry mediator is built from aCollection<Handler>— never an injected map, no reflection. There is deliberately no repository-wide command bus: one would make every module's vocabulary visible to every adapter and quietly re-merge the contexts. - Boundary types, not entities, cross the line. Commands, queries, results and
events in
domain/contractare dedicated types (records, ideally). Entities and value objects stay internal todomain/core. When a driven port genuinely needs a rich domain type in its signature, that type moves todomain/contractdeliberately — a design decision, not a default. domain/coredepends only on its owndomain/contractand implements its driving ports. Framework-free: constructor injection of driven ports, plain Java, exhaustively unit tested without any container.- Transaction boundaries are a domain decision expressed as a port. The settled
shape is a dedicated
UnitOfWorkdriven port indomain/contract—<T> T inTransaction(Supplier<T> work)— rather than unit-of-work semantics smeared over the repository port: a handler wraps the state-changing work of one command in it and every port touched inside commits or rolls back together, while how atomicity is achieved (JTA, aTransactionTemplate, the framework's transaction operations) stays an adapter ininfra/unit-of-work-<tech>. Keeping it a port of its own is what lets the handler be unit-tested against a counting fake. A transaction never spans two modules — treat the service seam as if it were already remote. - One domain per bounded context, several contexts per repository. A module that wants a slice of another module's use cases is asking a context-mapping question, and the answer is a port at the seam — not a wider contract module.
Details
What goes where
| Artifact | Module | Notes |
|---|---|---|
Command / Query / Handler / Mediator types |
platform/kernel |
the dispatch vocabulary, shared by every context, business-free |
The registry Mediator implementation |
platform/kernel |
generic; instantiated per module by the assembly |
| Sealed command/query records | modules/x/domain/contract |
name every operation this module supports |
| Driving ports | modules/x/domain/contract |
usually just the module's Mediator binding |
| Driven ports | modules/x/domain/contract |
OrderRepository, PaymentGateway, and peer-facing ports like OrderLookup |
| Command / query / result DTOs | modules/x/domain/contract |
immutable records; validated on construction |
| Domain events | modules/x/domain/contract |
published through a driven port |
| Domain errors | modules/x/domain/contract |
part of the contract — adapters must map them |
| Entities, value objects | modules/x/domain/core |
never leak; mapped at the boundary |
| Handlers | modules/x/domain/core |
self-declare via supports() |
| Domain services | modules/x/domain/core |
plain classes, constructor-injected driven ports |
Both port directions in the contract module, with their callers and implementors:
flowchart LR
CALLERS["user-side adapters (api, consumers, service)"]
KERNEL["platform/kernel<br/>Command · Handler · Mediator"]
subgraph contract["modules/x/domain/contract"]
DRIVING["Driving seam (the module's Mediator)"]
COMMANDS["Sealed commands & queries"]
DRIVEN["Driven ports (incl. peer-facing ones)"]
end
subgraph core["modules/x/domain/core"]
HANDLERS["Handlers (entities and value objects stay inside)"]
end
IMPLEMENTORS["modules/x/infra"]
CALLERS -->|"dispatch commands"| DRIVING
COMMANDS --> KERNEL
HANDLERS -->|"handle"| COMMANDS
HANDLERS -->|"depend on"| DRIVEN
IMPLEMENTORS -->|"implement"| DRIVEN
Dispatch — Command/Query + Mediator, per module
Java is the pattern's home turf, and the ruling makes it the default:
- Why it fits: sealed interfaces + records give genuinely sealed command
hierarchies; CDI/Spring collect the
Collection<Handler>for free; one dispatch seam gives uniform cross-cutting (transactions, validation, audit) by decorating the mediator — no AOP; and the module graph stays honest, since adapters depend on the command vocabulary indomain/contractand never ondomain/core. - Why per module (2026-08-14): the mediator's registry is exactly as wide as the
set of commands it can dispatch, so a global one would hand every adapter every
context's vocabulary — the opposite of what
modules/is for. Per-module mediators also make extraction free: the module leaves with its own seam intact. The assembly distinguishes them with a qualifier (@Ordering Mediator) or by constructing them explicitly. - Cost accepted: one unchecked cast inside the dispatcher; a missing handler is a runtime wiring error surfaced at first dispatch (the Rust realization does better — its compiler is the registry; see the Rust reference).
- Rejected as default — per-use-case driving interfaces (
PlaceOrder,CancelOrder): finer-grained dependency truth per adapter, but every cross-cutting concern is re-plumbed per interface and each new use case touches every wiring site. They remain the right shape in Go and the frontend, where the mediator object fights the platform — the stances are recorded per reference. - Rejected — one bus for the whole modulith: it reads as convenience and behaves as a shared domain. Modules that need each other use the service seam, where the dependency is visible in the build graph.
Testing
domain/core: plain JUnit against the module's commands, driven ports faked in-memory. This is where the bulk of the service's tests live, and they run in milliseconds. A context's tests never mention another context.domain/contract: nothing to test beyond invariants encoded in command constructors. Deliberately no contract-test kit binding infrastructure implementations to the ports (2026-08-09 ruling): adapters are integration-tested against their real technology on their own terms (see infrastructure layer); the in-memory fakes encode only what the domain relies on, and drift between a fake and a real adapter is handled when observed, not with standing machinery. The one near-exception is the peer gateway test described in module composition.platform/kernel: the registry mediator's own dispatch behavior — handler selection, the missing-handler failure — tested once for the whole repository.
Related
- Hexagonal architecture reference implementation — Java — the summary note this details.
- User-side adapters & assemblies — callers of the dispatch seam.
- Infrastructure layer — implementors of the driven ports.
- Module composition — how a driven port becomes a call into a peer module.
- Hexagonal architecture — the dependency rule this module split enforces.
- DDD — the tactical patterns living inside
domain/core, and the bounded contextsmodules/is carved along. - TDD — the framework-free core is what makes outside-in test-first practical.