6 The running hexagon: From pieces to system
The chapter explains how the hexagonal parts of WriteFlow move from isolated, well-defined components to a fully running application. It begins by showing that architectural boundaries need build-time enforcement, which is why the project is split into Maven modules for the shared kernel, application core, adapters, and a separate assembly module. This structure makes the dependency direction explicit, prevents inner code from depending on outer frameworks, and keeps the core portable and testable.
From there, the focus shifts to assembly, where Spring Boot is used only as the wiring mechanism. The assembly module discovers application services, adapters, and domain services through custom stereotype scanning, registers command and query buses, and provides external infrastructure beans such as clients and data sources. It also resolves the chapter’s most important runtime concern: transaction management. A technology-agnostic transactional annotation from the Architecture Kernel is bridged to Spring so that handlers can control transactional boundaries even when they return functional error values instead of throwing exceptions, preserving both rollback behavior and the separation from Spring-specific APIs.
The final part traces how events behave once the system is running. Domain events are delivered in-process to listeners, which can either participate in the same transaction or run after commit depending on the desired coupling. When a reaction must cross process or bounded-context boundaries, the chapter introduces integration events and the outbox pattern, which store the event alongside the business change in the same transaction and relay it later for reliable delivery. By combining modular structure, dependency injection, transactional control, local domain events, and durable integration events, the application becomes a coherent system rather than a collection of disconnected pieces.
Module dependency graph. Inner modules never reference outer ones. The assembly module is the only component that sees everything—its job is to wire, not to contain logic.
Assembly and runtime flow. At startup, Spring discovers beans through stereotype annotations, and the bus assembler creates buses backed by the application context. At runtime, the bus resolves handlers by command type—adapters never reference handlers directly.
The assembled WriteFlow hexagon. The assembly module wires the application at startup, while the runtime structure still respects the hexagonal boundary: input adapters enter through input ports, the bus dispatches to handlers, handlers orchestrate domain behaviour and output ports, and output adapters provide the infrastructure implementations.
Synchronous listeners execute inside the transaction; after-commit listeners execute after. The after-commit listener enters the application as a new use case through the command bus.
From handler to external delivery. The handler calls an output port whose adapter writes the integration event to the outbox within the same transaction. A relay process delivers it to external consumers. Consistency is guaranteed because the outbox write shares the domain state's transaction.
Summary
- Maven modules enforce hexagonal boundaries at compile time. If a dependency is not in the POM, the compiler rejects the import—no runtime discipline required.
- The spring-boot-assembly module is the only component that sees all others. It discovers hexagonal stereotypes (@ApplicationService, @Adapter, @DomainService) through annotation-based component scanning and wires the command and query buses backed by Spring's ApplicationContext.
- The Architecture Kernel's @Transactional annotation declares transactional intent without importing framework concerns. A custom transaction interceptor bridges the gap at runtime—and solves the Either problem: rollback by value, not by exception.
- Transaction boundaries always live at the handler level. The handler is the atomic unit of work—everything it orchestrates succeeds or fails together.
- Domain events decouple secondary effects from the primary use case. After-commit listeners execute only when the primary transaction succeeds, preventing secondary failures from rolling back the primary operation.
- Event listeners are input adapters. They receive a domain event and enter the application core through the command bus—same pattern as a REST controller, different trigger.
- Integration events escape the process boundary. An output port writes them to an outbox table within the same transaction as the domain state change. A relay process delivers them externally. Consumers must be idempotent.
- The assembly module draws a clear line: the application core compiles, runs its logic, and returns results without knowing which framework assembled it.
Clean Applications with Hexagonal Architecture ebook for free