Overview
The driving side of the modulith is split across two places, and the split is the whole
design. modules/<context>/user-side/ holds a module's primary adapters — REST
resources, message consumers, the in-process service — each as an ordinary library.
application/<typology> holds the assemblies: api, consumer, cron, each a
runnable, containerised deployment unit that mounts the matching adapters of every
module it composes and wires them to infrastructure. Adapters say how the outside
reaches a module; assemblies say what runs together.
Key points
- A module's adapters are never runnable. No
main, no framework bootstrap, noapplication.propertiesunderuser-side/. Everything there is a library, which is what lets one adapter be mounted by different assemblies — or by none, after a carve-out. - One deployment unit per delivery typology, not per adapter. Six modules exposing
HTTP produce one
application/apicontainer, not six. Scaling and rollout are decided per typology; a module that genuinely needs its own cadence is asking for extraction, not for a seventh assembly. - A module pair where the contract is real.
user-side/api/contractcarries the transport contract: request/response DTOs, the OpenAPI spec or generated interfaces for REST,.protofiles and stubs for gRPC, message schemas for consumers.user-side/api/adapterscarries the implementation: resources/controllers, DTO ↔ command mapping, error translation. - Why the split: the contract module is publishable. A client team — or the module
itself after extraction — depends on
ordering-user-side-api-contractwithout ever seeing the implementation, and contract-first workflows get a natural home for generated sources. - The pair is earned, not imposed. An adapter with no consumable contract — a cron
entry point, an internal batch — is a single module; an empty
contract/would be ceremony. The test: could another team meaningfully depend on that artifact? user-side/serviceis a peer of the others, treated in full in module composition: same job, different transport.- Adapters translate, never decide. An adapter maps transport DTOs to domain
commands, dispatches them through the module's
Mediatorfromdomain/contract, and maps results back. Business rules in a controller are a smell — push them through the port. - The assembly is the only assembly point, and it absorbed the flat layout's
configurator: it is the one module allowed to see adomain/corealongside the infrastructure modules. There is no separate wiring module, because the deployment unit already is one. - No adapter depends on another adapter, within a module or across modules. Cross-module reuse happens at the service seam, or not at all.
Details
Anatomy of one module's user side
modules/ordering/user-side/
├── api/
│ ├── contract/
│ │ ├── pom.xml # no domain deps; transport contract only
│ │ └── src/main/java/…/api/
│ │ ├── OrderRequest.java # DTOs — transport-shaped, not domain-shaped
│ │ ├── OrderResponse.java
│ │ └── openapi.yaml # or generated interfaces from it
│ └── adapters/
│ ├── pom.xml # depends on: api/contract, domain/contract
│ ├── README.md · AGENTS.md
│ └── src/main/java/…/
│ ├── OrderResource.java # dispatches commands through the mediator
│ └── OrderApiMapper.java # DTO ↔ command/result mapping
├── consumers/ # message consumers (+ a contract module
│ # when the schemas are published)
└── service/ # the in-process adapter — see module-composition
Notice what is absent: no Dockerfile, no main class, no runtime configuration. Those belong to whichever assembly mounts this adapter.
Anatomy of one assembly
application/api/
├── pom.xml # lists every module it composes
├── Dockerfile # the deployment unit's container definition
├── README.md # how to run THIS unit
├── AGENTS.md
└── src/
├── main/java/…/
│ ├── ApiApplication.java # the main class
│ └── composition/ # one configuration class per module
│ ├── OrderingCompositionRoot.java
│ └── BillingCompositionRoot.java
├── main/resources/application.properties
└── test/java/…/ # ArchUnit rules + end-to-end tests
The assembly's dependency list is its ops runbook: read application/consumer's POM
and you know it ships Kafka and Postgres but not the billing HTTP client.
The fan-out — three assemblies over two modules (an arrow reads "depends on"):
flowchart TB
subgraph apiunit["application/api — one container"]
API["assembly + Dockerfile"]
end
subgraph consumerunit["application/consumer — one container"]
CON["assembly + Dockerfile"]
end
subgraph cronunit["application/cron — one container"]
CRO["assembly + Dockerfile (no consumable contract anywhere)"]
end
subgraph ordering["modules/ordering/user-side"]
OA["api/adapters"]
OC["consumers"]
end
subgraph billing["modules/billing/user-side"]
BA["api/adapters"]
BCRON["cron"]
end
API --> OA
API --> BA
CON --> OC
CRO --> BCRON
Composition classes, one per module
Keep the assembly declarative: one configuration class per composed module, binding that module's driven ports to chosen adapters and constructing its mediator over its handlers. Splitting per module rather than per concern means an extraction deletes one file rather than editing five, and it keeps the assembly readable as a list of "which contexts am I hosting".
If logic accumulates in a composition class, it belongs in a domain. If wiring duplicates across assemblies, that is acceptable — the house rule prefers duplication between deployment units over a shared wiring module that couples them.
Per-module framework configuration
An adapter library still needs framework metadata (CDI beans.xml, Spring
auto-configuration entries, component scan roots). That metadata ships with the
adapter module, so mounting it is a dependency line and nothing more. The assembly
selects modules; it never reaches inside them to configure their internals.
Related
- Hexagonal architecture reference implementation — Java — the summary note this details.
- Module composition — the in-process service seam — the
user-side/serviceadapter and how assemblies wire two modules together. - Domain layer — contract & implementation — the mediator and driving ports these adapters call.
- Infrastructure layer — secondary adapters — the modules an assembly binds the driven ports to.
- Deployment & IaC — how each assembly's Dockerfile and stack definition travel with it.
- Backend protocols in Java — the transport implementations (REST, gRPC, messaging) these adapters are built with.
- API documentation in Java — the contract
formats (OpenAPI, AsyncAPI, proto) the
contractmodules carry, and the code-first vs contract-first workflows they anchor.