A learning project: moving from controller-service-repository habits to a hexagonal design, one failing test at a time.
Work in progress. The domain is complete for now and the application layer has started. There is no REST API, database or Spring Boot application yet; they are planned and listed below.
The goal is to learn to tell business rules from coordination, ports from adapters, and dependency direction from call direction. The end state is a small order-management core that can run behind REST and PostgreSQL, yet compiles and tests with neither.
compile-time dependencies bootstrap → adapters → application → domain → (JDK)
runtime calls REST adapter → service → OrderRepository port ← JPA adapter implements it
Each layer is a Maven module, and a module's POM lists only what it may depend on. An inward import from domain to anything else is a compile error, not a code-review comment. At runtime the service calls the repository, but at compile time the repository adapter depends on the core's interface. That reversal is the dependency inversion the exercise is about.
| In controller-service-repository terms | Hexagonal name | Who owns the interface |
|---|---|---|
| Controller | Driving adapter | Outside the core |
| Service | Application service behind an input port | Core |
| Repository interface | Output port, plain Java | Core, not Spring Data |
| JPA repository implementation | Driven adapter | Outside the core |
| Entity with getters and setters | Domain model with behavior | Core |
| Spring configuration | Bootstrap wiring | Outside the core |
| Layer | Status | What exists |
|---|---|---|
domain |
Built | Order aggregate, OrderItem, OrderStatus. Depends on the JDK only. |
application |
In progress | CreateOrderService turns a request into an order. The OrderRepository output port is next. |
| adapters | Planned | In-memory repository, REST controller, JPA / PostgreSQL repository. |
| bootstrap | Planned | Spring Boot wiring and Docker Compose for PostgreSQL. |
The domain enforces: an order needs an ID, a customer reference, a creation time and at least one non-null item; items are copied and exposed read-only; the total uses exact decimal arithmetic; and an order can be cancelled once. It deliberately does not generate IDs, read the clock, remember orders, or know whether a customer exists.
The application layer is being built on a separate branch and will be merged once its first use case is complete.
Test-driven, one small increment per cycle, trunk-based:
- Name the gap: what works, what is missing, and why this behavior comes next.
- Red: the smallest failing test that demands the behavior.
- Green: the minimum code that passes. A hidden dependency stays hidden until a test can observe it.
- Refactor: behavior-preserving, in its own commit.
- Run the focused test, then the whole build, before committing.
The commit history reads as the lesson: each feat commit adds one rule, each refactor commit changes no behavior.
A few decisions are recorded so they are not reopened every step. The service trusts the caller on customer identity. There is no input port or command object until something needs one. Clock will be injected next, while the UUID source stays hidden until a test cares what the ID is.
Review notes are in docs/refactoring.
Requires Java 21 and Maven.
mvn clean test- Creation time comes from an injected
Clock. - The created order is saved through an
OrderRepositoryoutput port, observed with a hand-written test double. - Get-order and cancel-order use cases.
- An in-memory repository adapter, then a REST adapter and bootstrap wiring.
- A PostgreSQL / JPA adapter, swapped in without touching the core.