Add the hexagonal module: Hexagonal Architecture (Ports and Adapters) in Spring Boot 4
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
# 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).
|
||||
Reference in New Issue
Block a user