Files
spring-boot-demo/hexagonal/README.md
T
Claude 83d245e726 hexagonal: capture the real missing-scan-base-packages failure as a transcript
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.
2026-10-03 20:35:59 +00:00

87 lines
5.2 KiB
Markdown

# 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 |
| [`app/output/01-missing-scan-base-packages-failure.txt`](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 `@...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).