4.8 KiB
hexagonal
Companion project for the article Hexagonal Architecture (Ports and Adapters) in Spring Boot 4 on ankurm.com.
There is deliberately no docs/ folder: the deeper material lives in collapsible "going
deeper" sections inside the article itself, next to the paragraph each one extends.
This is also the one project in this repo that is its own small Maven reactor rather than a
single self-contained project. That is not a style choice -- it is what makes "the core has no
Spring dependency" something Maven enforces, not a naming convention a reviewer has to remember
to check. Run mvn -pl core dependency:tree yourself and compare it to core/output/01-core-dependency-tree.txt.
Versions
| Spring Boot | 4.1.1 |
| Spring Framework | 7.0.9 |
| JDK | 25 (Temurin 25.0.4.1+1) |
| Maven | 3.9 |
| H2 | 2.4.240 (the version Boot 4.1.1 manages) |
| Hibernate ORM | 7.4.5.Final |
Modules
| Module | What it is | Depends on | Spring on its compile classpath? |
|---|---|---|---|
core/ |
the domain model, the inbound/outbound ports, WalletService |
nothing in this reactor | no -- mvn -pl core dependency:tree has zero Spring artifacts |
persistence-adapter/ |
the outbound adapter: WalletEntity, SpringDataWalletRepository, JpaWalletRepositoryAdapter |
core |
yes -- Spring Data JPA, H2 |
web-adapter/ |
the inbound adapter: WalletController, WalletExceptionHandler |
core |
yes -- Spring MVC |
app/ |
the composition root: HexagonalApplication, the only @SpringBootApplication in this project |
core, persistence-adapter, web-adapter |
yes -- it wires everything together |
Quickstart
export JAVA_HOME=/path/to/jdk-25
cd hexagonal
mvn clean install # builds all four modules in order, runs every test
To run the real application:
cd app
mvn spring-boot:run
curl -X POST "http://localhost:8080/wallets/w1/deposit?amountCents=500"
(there's no endpoint to create a wallet first -- the H2 schema starts empty; the application
test seeds one directly through WalletRepository before calling HTTP, which is itself worth
reading as an example of testing through the port rather than through a seed script.)
Captured output
| File | What it shows |
|---|---|
core/output/00-core-tests-no-spring.txt |
WalletServiceTest's 4 tests, no Spring context anywhere, finishing in milliseconds |
core/output/01-core-dependency-tree.txt |
the full mvn dependency:tree for hexagonal-core -- JUnit and AssertJ, nothing else |
persistence-adapter/output/00-jpa-adapter-tests.txt |
@DataJpaTest against a real H2 database: a save-then-find round trip and an unknown-id lookup |
web-adapter/output/00-webmvctest-mockmvc.txt |
@WebMvcTest with the ports mocked: the real JSON a deposit and a rejected withdrawal return |
app/output/00-end-to-end-http.txt |
the full stack over real HTTP: deposit, withdraw, an overdraft rejected with 409, and the balance reloaded straight from the database to cross-check the HTTP response |
A note for anyone copying this reactor shape
Two things cost real time while building this that are worth stating plainly rather than leaving a reader to rediscover them:
- Spring Boot 4 split most
@...Testannotations out ofspring-boot-test-autoconfigureinto their own small artifacts, each under a new package too (@DataJpaTestis noworg.springframework.boot.data.jpa.test.autoconfigure.DataJpaTestin artifactspring-boot-data-jpa-test;@WebMvcTestisorg.springframework.boot.webmvc.test.autoconfigure.WebMvcTestinspring-boot-webmvc-test).TestRestTemplatesurvived as itself but moved toorg.springframework.boot.resttestclient.TestRestTemplatein artifactspring-boot-resttestclient, and it is no longer auto-configured on@SpringBootTest(webEnvironment = RANDOM_PORT)--app's test needs an explicit@AutoConfigureTestRestTemplate. - In a reactor where the
@SpringBootApplicationclass's package is a sibling of the persistence adapter's package rather than an ancestor, Spring Data JPA's repository scan and JPA's entity scan both default to the main class's own package -- notscanBasePackages.HexagonalApplicationneeds an explicit@EnableJpaRepositories(basePackages = "...")and@EntityScan(basePackages = "...")pointing atcom.ankurm.hexagonal.persistence, or the adapter bean fails to wire with no hint that scanning is the cause.
Licence
MIT -- see the root LICENSE.