
by Zelkulon14 March 20251 min read
Clean Architecture by Robert C. Martin consistently implemented in Java/Spring Boot: layers, dependencies and why the domain must know nothing about Spring.
The Four Layers
ββββββββββββββββββββββββββββββββββββ
β Frameworks & Drivers β (Spring, JPA, REST)
β βββββββββββββββββββββββββββββββ β
β β Interface Adapters β β (Controllers, Repositories)
β β βββββββββββββββββββββββββ β β
β β β Application/UseCasesβ β β (Business Workflow)
β β β βββββββββββββββββββ β β β
β β β β Domain β β β β (Entities, Rules)
β β β βββββββββββββββββββ β β β
β β βββββββββββββββββββββββββ β β
β βββββββββββββββββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββ
Dependency rule: arrows always point INWARD.
The domain knows neither Spring nor JPA.Project Structure
order-service/
βββ src/main/java/com/zelkulon/order/
βββ domain/ β Innermost layer (no Spring!)
β βββ model/
β β βββ Order.java
β βββ valueobject/
β βββ Money.java
β
βββ application/ β Use Cases
β βββ port/
β β βββ in/ β Incoming ports
β β β βββ PlaceOrderUseCase.java
β β βββ out/ β Outgoing ports
β β βββ OrderRepository.java
β βββ service/
β βββ PlaceOrderService.java
β
βββ adapter/ β Outermost layer
βββ in/web/
β βββ OrderController.java
βββ out/persistence/
βββ OrderJpaEntity.java β JPA only here!
βββ OrderPersistenceAdapter.javaThe Domain: Spring-Free
// NO Spring annotations, NO JPA!
public class Order {
private final OrderId id;
private final CustomerId customerId;
private final List<OrderItem> items;
private OrderStatus status;
public Order(OrderId id, CustomerId customerId, List<OrderItem> items) {
if (items == null || items.isEmpty())
throw new OrderException("Mindestens ein Artikel nΓΆtig");
this.id = id;
this.items = List.copyOf(items);
this.status = OrderStatus.PENDING;
}
// Domain logic belongs in the domain!
public void confirm() {
if (this.status != OrderStatus.PENDING)
throw new OrderException("Nur PENDING kann bestΓ€tigt werden");
this.status = OrderStatus.CONFIRMED;
}
}
// Value Object (Record)
public record Money(BigDecimal amount, Currency currency) {
public Money {
if (amount.compareTo(BigDecimal.ZERO) < 0)
throw new IllegalArgumentException("Betrag darf nicht negativ sein");
}
public Money add(Money other) {
return new Money(amount.add(other.amount), currency);
}
}Ports and Use Case
// Incoming port (Use Case Interface)
public interface PlaceOrderUseCase {
OrderId placeOrder(PlaceOrderCommand command);
}
// Use Case Implementation
@UseCase // custom annotation, semantically clearer than @Service
@Transactional
@RequiredArgsConstructor
public class PlaceOrderService implements PlaceOrderUseCase {
private final OrderRepository orderRepository; // Interface!
private final PaymentGateway paymentGateway; // Interface!
@Override
public OrderId placeOrder(PlaceOrderCommand command) {
var order = new Order(
OrderId.newId(), command.customerId(), mapItems(command)
);
OrderId saved = orderRepository.save(order);
paymentGateway.initiatePayment(order);
return saved;
}
}Dependency Inversion Visualized
Without Clean Architecture: With Clean Architecture:
Controller Controller
β β
βΌ βΌ (implements)
OrderService βββ JPA/DB PlaceOrderUseCase (Port)
β β² (implements)
βΌ β
JpaRepository PlaceOrderService
β
OrderRepository (Port)
β² (implements)
β
OrderPersistenceAdapter
β
JpaRepositorySummary
- Testability: Domain and use cases testable without Spring (pure JUnit tests)
- Maintainability: Database changes do not affect the domain
- Replaceability: Framework migration possible without domain changes
- Readability: Every class has a clearly defined responsibility
- Longevity: Well-structured projects remain maintainable for years