Implémentation d'une Clean Architecture et du Domain-Driven Design (DDD) dans une application Spring Boot microservices
Dans cet article technique, nous expliquons comment combiner Clean Architecture et Domain-Driven Design (DDD) pour concevoir des microservices Spring Boot maintenables, testables et évolutifs. Nous abordons la structuration des modules, la séparation des responsabilités (domain, application, adapters, infrastructure), les patterns DDD (aggregates, value objects, domain events) et les considérations d’intégration (API, messaging, transactions, eventual consistency).
Principes clés
Séparer les couches
La Clean Architecture encourage la dépendance dirigée vers le domaine (le cœur). Vos couches typiques seront :
Domain (entités, règles métier), Application (cas d'utilisation), Adapters/Ports (interfaces et adaptateurs), Infrastructure (implémentations techniques).
Ubiquitous Language et Bounded Contexts
Utilisez un vocabulaire partagé entre équipes. Chaque microservice représente généralement un bounded context. Modélisez les aggregates et les invariants à l’intérieur du context.
Structure d’un microservice Spring Boot
Proposition de structure de modules Maven/Gradle (mono-repo ou multi-repo) :
service-order/
├─ order-domain/ (domain : entities, value objects, events)
├─ order-application/ (use-cases, DTOs, ports interfaces)
├─ order-adapters/ (rest controllers, message consumers/producers)
└─ order-infra/ (spring-data, jpa, kafka, configuration)
Exemples de code
Entité et Value Object (domain)
package com.example.order.domain;
public class Order {
private OrderId id;
private List<OrderLine> lines;
private OrderStatus status;
// invariants et méthodes métier ici
}
Port (interface repository) dans l’application/domain)
package com.example.order.application.port.out;
import com.example.order.domain.Order;
import java.util.Optional;
public interface LoadOrderPort {
Optional<Order> findById(OrderId id);
}
Adapter JPA (infrastructure)
package com.example.order.infra.jpa;
import com.example.order.application.port.out.LoadOrderPort;
import com.example.order.domain.Order;
import org.springframework.stereotype.Component;
@Component
public class OrderPersistenceAdapter implements LoadOrderPort {
private final SpringDataOrderRepository repo;
public OrderPersistenceAdapter(SpringDataOrderRepository repo) {
this.repo = repo;
}
@Override
public Optional<Order> findById(OrderId id) {
return repo.findById(id).map(this::toDomain);
}
private Order toDomain(OrderEntity e) { /* mapping */ }
}
Use Case / Application Service
package com.example.order.application;
import com.example.order.application.port.out.LoadOrderPort;
import com.example.order.domain.Order;
import org.springframework.stereotype.Service;
@Service
public class GetOrder {
private final LoadOrderPort loadOrder;
public GetOrder(LoadOrderPort loadOrder) {
this.loadOrder = loadOrder;
}
public Order execute(OrderId id) {
return loadOrder.findById(id)
.orElseThrow(() -> new OrderNotFoundException(id));
}
}
Controller REST (adapter)
package com.example.order.adapters.rest;
import com.example.order.application.GetOrder;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/orders")
public class OrderController {
private final GetOrder getOrder;
public OrderController(GetOrder getOrder) {
this.getOrder = getOrder;
}
@GetMapping("/{id}")
public OrderDto getOrder(@PathVariable String id) {
return OrderMapper.toDto(getOrder.execute(new OrderId(id)));
}
}
Communication inter-services
API Gateway et discovery
Utilisez un API Gateway (Spring Cloud Gateway) pour centraliser l’accès. Service discovery (Eureka, Consul) facilite le routage et la résilience.
Asynchrone et événements de domaine
Favorisez les domain events pour propager des changements entre services (Kafka, RabbitMQ). Cela permet l’eventual consistency et diminue le couplage synchrone.
Sagas / Orchestration
Pour des transactions distribuées, implémentez des sagas (orchestrator ou choreography) pour garantir la cohérence tout en conservant la résilience.
Tests et qualité
Stratégie recommandée :
- Unit tests pour les règles métier dans domain.
- Contract tests (Pact) pour APIs entre services.
- Integration tests pour adaptateurs (JPA, Kafka) via Testcontainers.
- End-to-end tests pour scénarios critiques.
Conseils pratiques
Isoler le domaine
Ne mettez aucune dépendance technique dans le module domain. Les dépendances doivent pointer vers le domain, jamais l’inverse.
Mapping et DTOs
Utilisez des DTOs dans les couches application/adapters pour éviter de propager les entités domaine en dehors du contexte.
Gestion des transactions
Privilégiez les transactions locales et la publication d’événements après commit. Pour des flux distribués, adoptez une orchestrations avec reprise sur erreurs.
Déploiement et observabilité
Chaque microservice doit posséder sa propre base de données (database-per-service). Ajoutez monitoring (Prometheus/Grafana), tracing distribué (OpenTelemetry/Jaeger) et logs structurés. Automatisez via CI/CD pour des déploiements reproductibles.
Conclusion
Combiner Clean Architecture et DDD dans une application Spring Boot microservices aide à maintenir des limites claires, une logique métier testable et une évolutivité accrue. La discipline de séparation des couches, le respect des bounded contexts et l’utilisation d’événements de domaine sont essentiels. Commencez petit, itérez sur vos aggregates et formalisez l’ubiquitous language entre équipes.
À propos de l'expert
Laty Gueye Samba est un développeur full stack basé à Dakar, passionné par l'architecture logicielle. Spécialiste des écosystèmes Java (Spring Boot) et Angular.