86 lines
4.8 KiB
Markdown
86 lines
4.8 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 |
|
|
|
|
## 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).
|