
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.javaLe 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
│
JpaRepositoryConclusion
- 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