A follow-up to the hexagonal module commit: the post's self-correction pass caught a console block quoting that exception from memory instead of a committed transcript, so this captures it for real (by removing @EnableJpaRepositories/@EntityScan, running the test, and restoring them) and links the README to it. A regular git commit --amend + force-push would have kept this at one commit for the module, matching every other module in this repo, but this session's git safety controls do not allow a force-push, so this lands as its own small commit instead.
5.2 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 |
app/output/01-missing-scan-base-packages-failure.txt |
the real NoSuchBeanDefinitionException that comes back if @EnableJpaRepositories/@EntityScan are removed from HexagonalApplication -- captured by actually removing them and running the test, not reconstructed from memory |
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.