# hexagonal Companion project for the article **[Hexagonal Architecture (Ports and Adapters) in Spring Boot 4](https://ankurm.com/hexagonal-architecture-ports-and-adapters-spring-boot-4/)** on **[ankurm.com](https://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/`](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/`](persistence-adapter) | the outbound adapter: `WalletEntity`, `SpringDataWalletRepository`, `JpaWalletRepositoryAdapter` | `core` | yes -- Spring Data JPA, H2 | | [`web-adapter/`](web-adapter) | the inbound adapter: `WalletController`, `WalletExceptionHandler` | `core` | yes -- Spring MVC | | [`app/`](app) | the composition root: `HexagonalApplication`, the only `@SpringBootApplication` in this project | `core`, `persistence-adapter`, `web-adapter` | yes -- it wires everything together | ## Quickstart ```bash 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: ```bash 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`](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`](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`](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`](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`](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 `@...Test` annotations out of `spring-boot-test-autoconfigure` into their own small artifacts, each under a *new* package too (`@DataJpaTest` is now `org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest` in artifact `spring-boot-data-jpa-test`; `@WebMvcTest` is `org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest` in `spring-boot-webmvc-test`). `TestRestTemplate` survived as itself but moved to `org.springframework.boot.resttestclient.TestRestTemplate` in artifact `spring-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 `@SpringBootApplication` class'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 -- not `scanBasePackages`. `HexagonalApplication` needs an explicit `@EnableJpaRepositories(basePackages = "...")` and `@EntityScan(basePackages = "...")` pointing at `com.ankurm.hexagonal.persistence`, or the adapter bean fails to wire with no hint that scanning is the cause. ## Licence MIT -- see the root [LICENSE](../LICENSE).