Overview
modules/<context>/infra/ holds that module's secondary (driven) adapters:
implementations of the driven ports declared in its domain/contract — persistence,
messaging producers, external API clients, and the gateways that reach peer modules.
Its shape is deliberately not fixed by the reference: granularity is decided ad-hoc,
per context — a single module or several more granular ones — because assemblies need
different subsets. This note records the forces on that decision and the shapes to
choose between (settled 2026-08-09; re-homed onto the modulith 2026-08-14).
Key points
- Infrastructure belongs to a module, not to the repository. There is no root
infrastructure/folder in the modulith: a Postgres adapter implements some context's repository port, and it lives with that context. Two contexts both using Postgres get two adapters, and that duplication is correct — they map different tables to different aggregates. - The forcing fact: assemblies need different subsets of the driven adapters. A consumer assembly that only writes to Postgres shouldn't ship the billing HTTP client; a read-only API assembly shouldn't ship the Kafka producer. Whatever shape is chosen must let each assembly's dependency list say exactly what it touches.
- Granularity is the trade-off itself — decide it ad-hoc. Both shapes are
legitimate: a single
inframodule where the context is small or its assemblies homogeneous; several granular modules — one per (port × technology), e.g.infra/persistence-postgres,infra/messaging-kafka,infra/client-billing— the day assemblies need different subsets. The reference refuses a fixed rule; the forcing question is always which assemblies need which adapters. - Ubiquitous ports live in
platform/, not in a module. Clock, id generation and the like have no business owner and no operational footprint;platform/commonsdeclares and implements them once for the whole repository. A module'sdomain/contractmay still re-declare a narrow port over them when it wants its own vocabulary, but the implementation is shared. - Peer gateways are ordinary driven adapters.
infra/ordering-gatewayimplements a port its own domain declared, exactly like a Postgres adapter does; it just talks toordering/user-side/serviceinstead of a database. It obeys every rule in this note, and it is the module's only legal reference to another module — see module composition. - Infra modules depend on their own
domain/contract, their technology, and nothing else (peer service modules excepted). Never on their owndomain/core, never on another module's domain, never on each other, never on an assembly. Shared plumbing between adapters (a common error mapper, shared Testcontainers setup) is a warning sign, not a convenience. - Each module ships its own configuration (Spring auto-configuration, CDI producers) so an assembly merely selects modules rather than knowing their internals.
- No contract testing between infra and the domain contract. Each module is integration-tested against its real technology (Testcontainers) on its own terms; the domain is tested against its in-memory fakes. The known risk — a fake drifting from real adapter behavior — is accepted and handled when observed, not with a standing contract-test kit.
Details
The three candidate shapes
| Shape | Pros | Cons | Verdict |
|---|---|---|---|
Single infra module per context |
trivial build, no ceremony | every assembly hosting the context drags every driver: fat images, wide attack surface, muddy test scope | legitimate while the context is small or its assemblies homogeneous |
One module per adapter under infra/ |
assemblies cherry-pick; per-adapter tests & deps; tech swap is localized | more POMs, more ceremony | legitimate the day assemblies need different subsets |
| One infra module per assembly | each assembly fully self-describing | duplicates adapters shared by several assemblies — same Postgres mapping code twice | rejected; the pick-list belongs in the assembly's POM, not in copied code |
The first two are a context call, not a progression with a "right" end state — that ad-hoc judgement is the design decision this layer asks of you. The third is out regardless.
Granularity heuristics (when going granular)
- Split per (driven port × technology) pair, not per port alone:
OrderRepositoryon Postgres isinfra/persistence-postgres; an alternative DynamoDB implementation isinfra/persistence-dynamodb, not a second package in the same module — the point is that an assembly's POM names its technologies. - One module may implement several small ports of the same technology — the
Postgres module can implement
OrderRepositoryandOrderHistoryQuery; splitting those would be ceremony without a consumer that wants one and not the other. Split when a real assembly wants the subset. - The module name states the technology, the package inside states the port.
Reading an assembly's dependency list should read like its ops runbook: postgres,
kafka, billing API. A nested
infra/<port>/<technology>directory pair says the same thing when the technology alone would be ambiguous — the scaffold emitsinfra/greeting-log/jdbcbesideinfra/unit-of-work/jta, and the Maven artifactId flattens either shape identically (greeting-infra-greeting-log-jdbc). - Never split along context lines you have not drawn. If two modules' adapters keep wanting to merge, the boundary between the contexts is the thing to re-examine — not the infrastructure.
Where the shared things live
| Concern | Home | Why not a module's infra/ |
|---|---|---|
| Clock, id generation, random | platform/commons |
no business owner; every context needs them identically |
| The dispatch vocabulary | platform/kernel |
it is the contract of the seam, not an adapter |
| Test fixtures for a technology | the adapter module's test sources | shared test plumbing across contexts couples their release cadence |
| A peer module's data | never — use the service seam | reading another context's tables is the failure this layout exists to prevent |
The adapter is not its connection
An infra module owns the mapping; the assembly owns the connection. Splitting them that way is what keeps the adapter a library:
| Thing | Home | Why |
|---|---|---|
OrderRepository implemented over SQL |
modules/x/infra/persistence-postgres |
it is the context's mapping of its own aggregate |
The DataSource / pool, its credentials, its profile config |
the assembly | pool sizing and secrets are deployment facts; two assemblies hosting the same context may size differently |
| The transactional boundary's implementation | modules/x/infra/unit-of-work-<tech> |
the port is the domain's (see domain layer); the framework's transaction machinery is an adapter |
The producer/@Configuration binding pool → adapter |
the assembly | the assembly is the only module allowed to see both a domain/core and an infra module |
| Schema migrations | their own deployment unit (migrations/) |
run against the database before the service deploys, never from inside it, so a bad migration blocks the rollout instead of the fleet — and schema-altering credentials never ship in a service image |
The adapter takes a DataSource in its constructor and knows nothing else. That is
what lets the same Postgres adapter serve an API assembly and a batch assembly whose
pools are configured differently — and it is why "each module ships its own
configuration" means its own beans, not its own infrastructure credentials.
The last row is the one worth enforcing loudly: no shared schema between modules. Each context owns its tables, and a context that needs a peer's data asks for it through a port. That rule costs almost nothing while everything is one database and is the single thing that makes extraction a migration rather than a rewrite.
Settled
- Ambient ports (
Clock, id generation): a shared platform module rather than per-port micro-modules or assembly-supplied defaults (2026-08-09; moved frominfrastructure/commonstoplatform/commonswith the modulith, 2026-08-14). - Contract testing between infra and the domain contract: ruled out (2026-08-09). Infra modules are integration-tested against their real technology on their own terms; the domain owns its fakes.
Related
- Hexagonal architecture reference implementation — Java — the summary note this details.
- Domain layer — the driven ports these modules implement.
- User-side adapters & assemblies — the assembly that selects among these modules per deployment unit.
- Module composition — the peer gateway, an infra adapter whose technology is another module.
- Storage access from Java — the persistence technologies the modules wrap.
- Testing in Java — Testcontainers-based integration testing per adapter module.