Architecture Hexagonale et Clean Code avec Spring Boot : Guide pour des projets maintenables
Introduction
Ce guide présente une approche pragmatique pour appliquer l'architecture hexagonale (ports et adaptateurs) associée aux principes de Clean Code dans des projets Spring Boot. L'objectif est de favoriser la maintenabilité, la testabilité et l'évolution du logiciel en séparant clairement la logique métier des mécanismes d'infrastructure.
Principes fondamentaux
Domain first et séparation des responsabilités
La logique métier (domaine) doit rester indépendante des frameworks et technologies. Les cas d'utilisation (application) orchestrent les règles métiers sans connaître les détails d'infrastructure. Les adaptateurs inbound/outbound implémentent des ports définis par le domaine ou l'application.
Dépendance inversion et testabilité
Les couches externes dépendent des ports (interfaces) définis au cœur. Cette inversion de dépendance permet d'isoler le domaine et d'écrire des tests unitaires rapides en simulant les ports. Les adaptateurs peuvent être testés séparément avec des tests d'intégration.
Organisation des couches dans un projet Spring Boot
Structure recommandée
Une organisation simple et cohérente facilite la navigation et les responsabilités :
src/main/java/com/example/
domain/ // entités, value objects, règles métier
application/ // cas d'utilisation, DTOs, interfaces (ports)
adapters/ // inbound (rest), outbound (persistence, messaging)
config/ // configuration Spring, beans partagés
Règle d'or
Le domaine ne doit contenir aucune dépendance Spring. Les classes du domaine restent de simples POJOs, ce qui garantit indépendance et portabilité.
Exemple minimal de code
Définition d'un port (interface)
public interface OrderPort {
Order save(Order order);
Optional<Order> findById(Long id);
}
Use case dans la couche application
public class CreateOrderUseCase {
private final OrderPort orderPort;
public CreateOrderUseCase(OrderPort orderPort) {
this.orderPort = orderPort;
}
public Order execute(CreateOrderCommand cmd) {
// validation et logique métier
return orderPort.save(cmd.toOrder());
}
}
Adaptateur de persistance (outbound)
public interface SpringOrderRepository extends JpaRepository<OrderEntity, Long> { }
@Repository
public class OrderRepositoryAdapter implements OrderPort {
private final SpringOrderRepository repository;
public OrderRepositoryAdapter(SpringOrderRepository repository) {
this.repository = repository;
}
public Order save(Order order) {
return repository.save(mapToEntity(order)).toDomain();
}
}
Adaptateur inbound (REST controller)
@RestController
@RequestMapping("/orders")
public class OrderController {
private final CreateOrderUseCase createOrderUseCase;
public OrderController(CreateOrderUseCase createOrderUseCase) {
this.createOrderUseCase = createOrderUseCase;
}
@PostMapping
public ResponseEntity<OrderDto> create(@RequestBody CreateOrderDto dto) {
Order order = createOrderUseCase.execute(dto.toCommand());
return ResponseEntity.status(HttpStatus.CREATED).body(OrderDto.from(order));
}
}
Tests et stratégies
Tests unitaires
Les use cases sont testés isolément en simulant les ports via des doubles (mocks ou stubs). Ces tests garantissent la logique métier sans dépendre d'une base de données ou d'un broker.
Tests d'intégration
Les adaptateurs sont testés avec des slices Spring (@DataJpaTest, @WebMvcTest) ou des tests d'intégration complets (@SpringBootTest). Les scénarios de bout en bout valident l'assemblage des couches.
Bonnes pratiques Clean Code
Noms explicites et petites fonctions
Préférer des noms clairs et concis pour les classes et méthodes. Les fonctions doivent rester petites et exprimer une seule intention. Le code lisible réduit le coût de la maintenance.
Éviter la duplication
Extraire les comportements communs dans des services réutilisables ou des utilitaires bien testés. Les DTOs et mappers doivent être simples et centralisés pour éviter la duplication des conversions.
Gestion des erreurs
Utiliser des exceptions métiers explicites et des classes de domaine pour représenter les invariants. Dans les contrôleurs, transformer ces exceptions en réponses HTTP appropriées via des handlers globaux.
Transactions et frontières
Définir clairement la frontière transactionnelle. Il est fréquent d'appliquer @Transactional sur les classes de l'application (use cases) exposées comme beans Spring afin de garantir l'atomicité des opérations métier, tout en gardant le domaine détaché des mécanismes transactionnels.
Évolution et maintenance
La modularité offerte par l'architecture hexagonale facilite l'évolution : remplacement d'une base de données, ajout d'une file de messages ou refonte d'une interface utilisateur sans impact sur le domaine. Les tests automatisés et la documentation des ports accélèrent la compréhension des changements.
Conclusion
En combinant l'architecture hexagonale et les principes de Clean Code, les projets Spring Boot gagnent en robustesse, testabilité et extensibilité. La séparation claire des responsabilités, l'utilisation d'interfaces pour inverser les dépendances et des conventions de code strictes contribuent à un code plus maintenable et à une livraison plus prévisible.
À propos de l'auteur
Laty Gueye Samba est développeur Full Stack basé à Dakar, Sénégal. Spécialiste des écosystèmes Java / Spring Boot et Angular.
Contact : latygueyesamba@gmail.com | Dakar, Sénégal