A Spring Boot REST API built as a time-constrained coding challenge (< half a day). Demonstrates Clean Architecture (Onion pattern) applied to a delivery tracking domain: framework-free domain, isolated use cases, and HTTP semantics driven by exception hierarchy.
Two user stories, delivered as a working, tested API:
Track delivery: As a customer, I can follow my delivery state from my account. Possible states:
ACCEPTED → READY → DELAYED / DELIVERING → DELIVERED.
Update delivery: As a customer, I can modify my delivery address or time slot, as long as my delivery state is
ACCEPTED(not yetREADY).
flowchart TD
HTTP([HTTP Request])
subgraph ADAPTER["ADAPTER (fr.kata.delivery.adapters)"]
C[Controllers / DTOs]
P[JPA Repositories]
EH[GlobalExceptionHandler]
end
subgraph APPLICATION["APPLICATION (fr.kata.delivery.application)"]
SVC[CustomerDeliveryService]
REPO[DeliveryRepository interface]
EX[UseCaseException hierarchy]
end
subgraph DOMAIN["DOMAIN (fr.kata.delivery.domain)"]
AGG[Delivery aggregate]
VO[Address · DeliverySlot · DeliveryId]
SM[DeliveryState machine]
end
HTTP --> C
C --> SVC
SVC --> REPO
SVC --> AGG
REPO -.->|implemented by|P
style ADAPTER fill: #dbeafe, stroke: #3b82f6
style APPLICATION fill: #dcfce7, stroke: #22c55e
style DOMAIN fill: #fef9c3, stroke: #eab308
Each layer depends only inward. The domain has no knowledge of Spring, JPA, or HTTP. Replacing H2 with PostgreSQL, or swapping REST for a message consumer, requires changes only in the ADAPTER layer.
| Layer | Package | Responsibility |
|---|---|---|
| ADAPTER | adapters/controllers |
REST endpoints, DTO mapping, exception → HTTP |
| ADAPTER | adapters/persistence |
JPA entities, DeliveryJpaMapper, JpaDeliveryRepository |
| APPLICATION | application |
CustomerDeliveryService: owns use-case logic and ownership checks |
| APPLICATION | application |
DeliveryRepository: port interface; never touches JPA |
| DOMAIN | domain |
Aggregate root, Value Objects, state machine, DomainException |
| CONFIG | config |
ApplicationConfig: Spring bean wiring only |
Domain proof: Value Objects validate themselves, zero framework dependency:
// DOMAIN layer: no Spring, no JPA
public record Address(String line1, String line2, String postalCode, String city, String countryCode) {
public Address {
if (line1 == null || line1.isBlank()) throw new DomainException("Address.line1 must be provided");
if (postalCode == null || postalCode.isBlank())
throw new DomainException("Address.postalCode must be provided");
// ...
countryCode = countryCode.trim().toUpperCase();
}
}@NotNull on a DTO is adapter-layer validation. Business rules live here.
Context: Business logic risked coupling to Spring/JPA annotations if no boundary was enforced.
Decision: Three concentric layers (ADAPTER → APPLICATION → DOMAIN), dependencies inward only. Domain layer imports zero framework classes.
Consequence: Domain and application layers are testable without a Spring context. Infrastructure is swappable without touching business rules.
Context: Using @Service / @Component in the application layer would introduce Spring coupling where there should be none.
Decision: ApplicationConfig instantiates CustomerDeliveryService explicitly and injects the JPA repository adapter.
Consequence: Application and domain layers remain framework-agnostic. The config package is the only place that knows both sides.
Context: Canonical constructors on record types run on every instantiation (no risk of bypassing validation).
Decision: All Value Objects (Address, DeliverySlot, DeliveryId, CustomerId) throw DomainException directly in their canonical constructors.
Consequence: Invalid domain objects cannot be created. CustomerDeliveryService catches DomainException and re-throws as BusinessRuleViolationException (409) to preserve layer boundaries.
Context: Domain and use-case failures must surface as correct HTTP status codes without leaking framework concerns into the application layer.
Decision: UseCaseException hierarchy: EntityNotFoundException (404), OperationNotAllowedException (403), BusinessRuleViolationException (409). GlobalExceptionHandler maps each subtype to its HTTP code.
Consequence: HTTP semantics are decided once, in the adapter layer. Application layer throws business-meaningful exceptions; HTTP codes are not its concern.
Prerequisites: JDK 21, Maven 3.9+
git clone <repo-url>
cd KataDelivery
mvn spring-boot:runVerify with the Swagger UI: http://localhost:8080/swagger-ui/index.html
All endpoints require an X-Customer-Id header (missing header → 401).
| Concern | Technology |
|---|---|
| Language | Java 21 |
| Framework | Spring Boot 3 |
| Build | Maven 3.9 |
| Database | H2 (in-memory) |
| ORM | Spring Data JPA + @Version optimistic locking |
| API docs | OpenAPI / Swagger UI |
mvn testdomain/: pure unit tests, no Spring contextapplication/:InMemoryDeliveryRepositorystub, no Spring contextKataDeliveryApplicationTests: Spring Boot context smoke test
Interactive documentation: Swagger UI (requires a local running instance)
GET /api/v1/deliveries/{deliveryId}— view delivery state and detailsPATCH /api/v1/deliveries/{deliveryId}— update address and time slot (ACCEPTEDstate only)
All endpoints require X-Customer-Id header. Missing header → 401 Unauthorized.