rgoussu@goussu: ~/library/java/hexagonal-reference-implementation
~/library/java/hexagonal-reference-implementation cat application-layer.md

User-side adapters & assemblies

# Each module's user-side/ holds its driving adapters as libraries — api contract+adapters where the contract is real, consumers, and the in-process service — while application/<typology> assembles them into the runnable deployment units.

Conceptsaved 2026-08-09updated 2026-08-14 #hexagonal-architecture#architecture#java#primary-adapters#deployment-unit#modulith

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, no application.properties under user-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/api container, 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/contract carries the transport contract: request/response DTOs, the OpenAPI spec or generated interfaces for REST, .proto files and stubs for gRPC, message schemas for consumers. user-side/api/adapters carries 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-contract without 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/service is 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 Mediator from domain/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 a domain/core alongside 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