Clean Architecture in Java: Domänenlogik vom Framework trennen

Clean Architecture in Java: Domänenlogik vom Framework trennen
https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
von Zelkulon14. März 20251 min Lesezeit

Clean Architecture nach Robert C. Martin konsequent umgesetzt in Java/Spring Boot: Schichten, Abhängigkeiten und warum die Domäne nichts von Spring wissen darf.

Die vier Schichten

            ┌──────────────────────────────────┐
            │        Frameworks & Drivers      │  (Spring, JPA, REST)
            │  ┌─────────────────────────────┐ │
            │  │    Interface Adapters       │ │  (Controller, Repositories)
            │  │  ┌───────────────────────┐  │ │
            │  │  │   Application/UseCases│  │ │  (Business-Workflow)
            │  │  │  ┌─────────────────┐  │  │ │
            │  │  │  │     Domain      │  │  │ │  (Entitäten, Regeln)
            │  │  │  └─────────────────┘  │  │ │
            │  │  └───────────────────────┘  │ │
            │  └─────────────────────────────┘ │
            └──────────────────────────────────┘

Abhängigkeitsregel: Pfeile zeigen IMMER nach innen.
Die Domäne kennt weder Spring noch JPA.

Projektstruktur

order-service/
└── src/main/java/com/zelkulon/order/
    ├── domain/                    ← Innerste Schicht (kein Spring!)
    │   ├── model/
    │   │   └── Order.java
    │   └── valueobject/
    │       └── Money.java
    │
    ├── application/               ← Use Cases
    │   ├── port/
    │   │   ├── in/                ← Eingehende Ports
    │   │   │   └── PlaceOrderUseCase.java
    │   │   └── out/               ← Ausgehende Ports
    │   │       └── OrderRepository.java
    │   └── service/
    │       └── PlaceOrderService.java
    │
    └── adapter/                   ← Äußerste Schicht
        ├── in/web/
        │   └── OrderController.java
        └── out/persistence/
            ├── OrderJpaEntity.java  ← Nur hier JPA!
            └── OrderPersistenceAdapter.java

Die Domäne: Spring-frei

// KEINE Spring-Annotationen, KEIN 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-Logik in der Domäne!
    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 und Use Case

// Eingehender Port (Use Case Interface)
public interface PlaceOrderUseCase {
    OrderId placeOrder(PlaceOrderCommand command);
}

// Use Case Implementation
@UseCase  // eigene Annotation, semantisch klarer als @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;
    }
}

Abhängigkeitsumkehr visualisiert

Ohne Clean Architecture:            Mit Clean Architecture:

Controller                          Controller
    │                                   │
    ▼                                   ▼ (implementiert)
OrderService ◄── JPA/DB            PlaceOrderUseCase (Port)
    │                                   ▲ (implementiert)
    ▼                                   │
JpaRepository                       PlaceOrderService
                                        │
                                    OrderRepository (Port)
                                        ▲ (implementiert)
                                        │
                                    OrderPersistenceAdapter
                                        │
                                    JpaRepository

Fazit

  • Testbarkeit: Domäne und Use Cases ohne Spring testbar (reine JUnit-Tests)
  • Wartbarkeit: Änderungen an der Datenbank berühren die Domäne nicht
  • Austauschbarkeit: Framework-Migration ohne Domänenänderungen möglich
  • Lesbarkeit: Jede Klasse hat eine klar definierte Verantwortung
  • Langlebigkeit: Sauber strukturierte Projekte bleiben über Jahre wartbar
Clean Architecture in Java: Domänenlogik vom Framework trennen