Clean Architecture in Java: Separating Domain Logic from the Framework

Clean Architecture in Java: Separating Domain Logic from the Framework
https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
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.java

The 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
                                        β”‚
                                    JpaRepository

Summary

  • 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
Clean Architecture in Java: Separating Domain Logic from the Framework