Clean Architecture en Java : Séparer la logique métier du framework

Clean Architecture en Java : Séparer la logique métier du framework
https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
par Zelkulon14 mars 20251 min de lecture

Clean Architecture de Robert C. Martin implémentée de manière cohérente en Java/Spring Boot : couches, dépendances et pourquoi le domaine ne doit rien savoir de Spring.

Les Quatre Couches

            ┌──────────────────────────────────┐
            │        Frameworks & Drivers      │  (Spring, JPA, REST)
            │  ┌─────────────────────────────┐ │
            │  │    Interface Adapters       │ │  (Contrôleurs, Dépôts)
            │  │  ┌───────────────────────┐  │ │
            │  │  │   Application/UseCases│  │ │  (Flux Métier)
            │  │  │  ┌─────────────────┐  │  │ │
            │  │  │  │     Domain      │  │  │ │  (Entités, Règles)
            │  │  │  └─────────────────┘  │  │ │
            │  │  └───────────────────────┘  │ │
            │  └─────────────────────────────┘ │
            └──────────────────────────────────┘

Règle de dépendance : les flèches pointent toujours VERS L'INTÉRIEUR.
Le domaine ne connaît ni Spring ni JPA.

Structure du Projet

order-service/
└── src/main/java/com/zelkulon/order/
    ├── domain/                    ← Couche la plus interne (sans Spring !)
    │   ├── model/
    │   │   └── Order.java
    │   └── valueobject/
    │       └── Money.java
    │
    ├── application/               ← Cas d'utilisation
    │   ├── port/
    │   │   ├── in/                ← Ports entrants
    │   │   │   └── PlaceOrderUseCase.java
    │   │   └── out/               ← Ports sortants
    │   │       └── OrderRepository.java
    │   └── service/
    │       └── PlaceOrderService.java
    │
    └── adapter/                   ← Couche la plus externe
        ├── in/web/
        │   └── OrderController.java
        └── out/persistence/
            ├── OrderJpaEntity.java  ← JPA uniquement ici !
            └── OrderPersistenceAdapter.java

Le Domaine : Sans Spring

// PAS d'annotations Spring, PAS de 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;
    }

    // La logique métier appartient au domaine !
    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 et Cas d'Utilisation

// Port entrant (interface de cas d'utilisation)
public interface PlaceOrderUseCase {
    OrderId placeOrder(PlaceOrderCommand command);
}

// Implémentation du cas d'utilisation
@UseCase  // annotation personnalisée, plus claire que @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;
    }
}

Inversion de Dépendance

Sans Clean Architecture :            Avec Clean Architecture :

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

Conclusion

  • Testabilité : Domaine et cas d'utilisation testables sans Spring (JUnit pur)
  • Maintenabilité : Les changements de base de données n'affectent pas le domaine
  • Remplaçabilité : Migration de framework sans modifier le domaine
  • Lisibilité : Chaque classe a une responsabilité clairement définie
  • Longévité : Les projets bien structurés restent maintenables des années
Clean Architecture en Java : Séparer la logique métier du framework