Retour aux articles

Implémentation d'une Clean Architecture et du Domain-Driven Design (DDD) dans une application Spring Boot microservices

Implémentation d'une Clean Architecture et du Domain-Driven Design (DDD) dans une application Spring Boot microservices

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.