This project is a small shop-ordering API designed to make hexagonal architecture easy to see.
The important rule is: business code does not depend on Spring MVC, Spring Data JPA, H2, or HTTP. Those technologies sit in adapters around the application core.
HTTP client
|
v
adapter/in/web
|
v
application/port/in <--- use-case interfaces
|
v
application/service <--- orchestration
|
v
application/port/out <--- persistence/number-generator contracts
|
v
adapter/out/*
domain/model
Pure business objects: Order, OrderLine, Product, Money. There are no Spring or JPA annotations here.
application/order/*
Order use cases. PlaceOrderService, GetOrderService, CancelOrderService, and DeleteOrderService each coordinate one use case and depend only on ports.
application/*/port/out
Outbound ports. These describe what the application needs from the outside world: loading products, loading orders, saving orders, deleting orders, and generating order numbers.
adapter/in/web
Inbound adapter. OrderController translates HTTP JSON into an application command.
adapter/out/persistence
Outbound adapter. JPA entities and Spring Data repositories translate between database rows and domain objects.
adapter/out/number
Outbound adapter. Generates order numbers.
config
Spring wiring. This is where Spring creates the application service and injects adapter implementations into its ports.
mvn spring-boot:runOpen the browser UI:
http://localhost:8080
The UI can:
- create orders
- show created orders in a table
- fetch an order by id
- cancel an order
- delete an order from the database
- fetch seeded products
- show the latest API response
If port 8080 is already in use, start on another port:
mvn spring-boot:run -Dspring-boot.run.arguments=--server.port=18080Then open:
http://localhost:18080
Create an order:
curl -i -X POST http://localhost:8080/orders \
-H 'Content-Type: application/json' \
-d '{
"customerEmail": "learner@example.com",
"items": [
{
"productId": "11111111-1111-1111-1111-111111111111",
"quantity": 2
}
]
}'The response includes the generated id. Use it to fetch the order:
curl http://localhost:8080/orders/{id}List created orders:
curl http://localhost:8080/ordersCancel an order:
curl -i -X POST http://localhost:8080/orders/{id}/cancelThis keeps the order in the database and changes its status to CANCELLED.
Delete an order:
curl -i -X DELETE http://localhost:8080/orders/{id}This removes the order from the database. A later GET /orders/{id} returns 404.
Fetch a product:
curl http://localhost:8080/products/11111111-1111-1111-1111-111111111111Run the tests:
mvn testRun the fast unit tests without Spring integration tests:
mvn test -Dtest='!*IntegrationTest'- Start with
PlaceOrderService. - Notice that it depends on interfaces from
application/port/out, not repositories. - Follow
LoadProductPorttoProductPersistenceAdapter. - Notice that JPA entities are not the domain model.
- Compare
CancelOrderServicewithDeleteOrderService; one changes domain state, the other removes persistence state. - Read the service tests; they test the use cases without Spring.
- Read
OrderControllerIntegrationTest; it tests full adapter-to-adapter flows.
The app starts with three H2 products from src/main/resources/data.sql:
| Product | ID |
|---|---|
| Clean Architecture Book | 11111111-1111-1111-1111-111111111111 |
| Domain-Driven Design Notes | 22222222-2222-2222-2222-222222222222 |
| Refactoring Mug | 33333333-3333-3333-3333-333333333333 |