A Spring Boot REST API built as a time-constrained coding challenge (< half a day). Demonstrates Clean
Architecture (Onion pattern) across two bounded contexts: order history and refund requests. Domain highlights include
Money arithmetic, evidence validation, and domain factory methods enforcing ownership rules.
Two user stories, delivered as a working, tested API:
Order history: As a customer, I can view the history of my orders. I can submit a refund request for a specific product.
Refund evidence: For refund requests, I must provide evidence: a photo and an issue description.
flowchart TD
HTTP([HTTP Request])
subgraph ADAPTER["ADAPTER (fr.kata.refund.adapters)"]
OC[OrderController]
RC[RefundController]
P[JPA Repositories]
EH[GlobalExceptionHandler]
end
subgraph APPLICATION["APPLICATION (fr.kata.refund.application)"]
OS[CustomerOrderService]
RS[CustomerRefundService]
OR[OrderRepository interface]
RR[RefundRepository interface]
end
subgraph DOMAIN["DOMAIN (fr.kata.refund.domain)"]
O[Order aggregate]
RF[RefundRequest aggregate]
M[Money · RefundEvidence]
VO[OrderId · ProductId · CustomerId]
end
HTTP --> OC
HTTP --> RC
OC --> OS
RC --> RS
OS --> OR
RS --> RR
OS --> O
RS --> RF
OR -.->|implemented by|P
RR -.->|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. Both bounded contexts (Order, Refund) share the same layered structure and dependency rule.
| Layer | Package | Responsibility |
|---|---|---|
| ADAPTER | adapters/controllers/order |
Order endpoints, DTOs |
| ADAPTER | adapters/controllers/refund |
Refund endpoints, evidence DTOs |
| ADAPTER | adapters/persistence |
JPA entities, mappers, JpaOrderRepository, JpaRefundRepository |
| APPLICATION | application/order |
CustomerOrderService, OrderRepository port |
| APPLICATION | application/refund |
CustomerRefundService, RefundRepository port |
| DOMAIN | domain/order |
Order aggregate, OrderItem, OrderStatus, Money |
| DOMAIN | domain/refund |
RefundRequest aggregate, RefundState, RefundEvidence, PhotoEvidence |
| CONFIG | config |
ApplicationConfig: Spring bean wiring only |
Domain proof: Money enforces monetary rules, zero framework dependency:
// DOMAIN layer: no Spring, no JPA
public record Money(BigDecimal amount, Currency currency) {
public Money {
if (amount == null || currency == null) throw new DomainException("Money amount/currency must be provided");
if (amount.scale() > 2) amount = amount.setScale(2, RoundingMode.HALF_UP);
if (amount.compareTo(BigDecimal.ZERO) < 0) throw new DomainException("Money amount must be >= 0");
}
public Money add(Money other) {
if (!currency.equals(other.currency)) throw new DomainException("Currency mismatch");
return new Money(amount.add(other.amount), currency);
}
}Currency mismatch, negative amounts, and rounding are domain rules, not DTO annotations.
Context: Two bounded contexts (Order + Refund) risked coupling business logic to Spring/JPA if no boundary was enforced.
Decision: Three concentric layers (ADAPTER → APPLICATION → DOMAIN), dependencies inward only. Domain layer imports zero framework classes.
Consequence: Both aggregates 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 CustomerOrderService and CustomerRefundService explicitly and injects JPA adapters.
Consequence: Application and domain layers remain framework-agnostic. The config package is the only place that knows both sides.
Context: Order total computation and line-item pricing require monetary arithmetic. Using double or raw BigDecimal primitives scattered across the codebase leads to silent rounding errors and cross-currency bugs.
Decision: Money(BigDecimal amount, Currency currency) encapsulates rounding (2 decimal places), non-negativity, and currency coherence. add() and multiply() enforce same-currency constraint at call time.
Consequence: No cross-currency arithmetic can silently succeed. Rounding policy is declared once in the domain.
Context: Submitting a refund requires two business invariants: the requester must own the order, and the product must exist in that order. Enforcing these in the service layer leaks domain rules into the application layer.
Decision: RefundRequest.submit(id, order, productId, requester, evidence, now) is a static factory on the aggregate. It verifies ownership and product membership before constructing the record.
Consequence: Invariants are co-located with the aggregate they protect. The service layer delegates rule enforcement to the domain and catches DomainException to re-throw as BusinessRuleViolationException (409).
Prerequisites: JDK 21, Maven 3.9+
git clone <repo-url>
cd KataRefund
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 |
| API docs | OpenAPI / Swagger UI |
mvn testdomain/: pure unit tests, no Spring contextapplication/:InMemoryOrderRepositoryandInMemoryRefundRepositorystubs, no Spring contextKataRefundApplicationTests: Spring Boot context smoke test
Interactive documentation: Swagger UI (requires a local running instance)
GET /api/v1/orders— list orders for the authenticated customerPOST /api/v1/refunds— submit a refund request with photo evidence
All endpoints require X-Customer-Id header. Missing header → 401 Unauthorized.