Chapitre 15.7 — DDD, CQRS & hexagonal
Partie 15 : Expert — Chapitre 7/9 Version de référence : Symfony 7.4.
⏱️ TL;DR
- Sur un domaine complexe, tout coupler à Doctrine/Symfony rend le code difficile à faire évoluer et à tester. Ces patterns isolent la logique métier.
- Hexagonal (ports & adapters) : le domaine au centre, l’infrastructure (base, HTTP) aux bords, reliés par des interfaces (ports). Le domaine ne dépend pas de Symfony/Doctrine.
- DDD fournit le vocabulaire : entités, value objects, agrégats, événements de domaine, repositories (interfaces dans le domaine, implémentés dans l’infra).
- CQRS sépare commandes (écriture) et requêtes (lecture) — souvent deux bus Messenger.
- N’over-engineerez pas : ces patterns sont pour les domaines riches, pas pour un CRUD simple.
- Pour un dev Next.js : c’est la « clean architecture » — séparer la logique métier des frameworks, exactement comme on isole la logique d’un composant de son UI.
🎯 Objectifs
- Comprendre l’architecture hexagonale et son intérêt.
- Connaître les briques DDD et leur place.
- Séparer commandes et requêtes (CQRS).
- Savoir quand appliquer ces patterns (et quand s’abstenir).
1. Le problème : le couplage à l’infrastructure
Dans un CRUD simple, coupler entités et logique à Doctrine/Symfony est parfait (rapide, direct). Mais quand le domaine devient riche (règles complexes, invariants, workflows), ce couplage pose problème : impossible de tester la logique sans la base, difficile de changer d’ORM ou d’exposer le domaine autrement, et la logique métier se noie dans la plomberie technique.
2. L’architecture hexagonale (ports & adapters)
L’idée : mettre le domaine (la logique métier pure) au centre, et repousser l’infrastructure (base, HTTP, e-mail) aux bords. Le domaine communique avec l’extérieur via des ports (des interfaces), et l’infrastructure fournit des adapters (implémentations).
Le point clé : le domaine dépend d’interfaces (BookingRepositoryInterface, PaymentGatewayInterface), pas de Doctrine ni de Stripe. L’infrastructure implémente ces interfaces. Résultat : on teste le domaine avec des doubles (Partie 14), on change l’implémentation (Doctrine → autre) sans toucher la logique, et la logique métier est lisible (pas de SQL au milieu).
C’est l’injection de dépendances (Partie 5) poussée jusqu’au bout : on injecte des interfaces, le domaine ignore le concret.
3. Les briques DDD
Le Domain-Driven Design fournit un vocabulaire pour modéliser un domaine :
| Brique | Rôle |
|---|---|
| Entité | objet avec une identité (un Booking) |
| Value Object | objet immuable défini par sa valeur (Money, DateRange, Email — chapitre 2.5) |
| Agrégat | grappe d’objets traitée comme un tout cohérent (racine + enfants), garantissant les invariants |
| Repository | abstraction de persistance (interface dans le domaine) |
| Événement de domaine | « un fait métier s’est produit » (BookingConfirmed) |
| Service de domaine | logique métier qui n’appartient à aucune entité |
Exemple d’invariant dans un agrégat : « on ne peut pas réserver plus de places qu’il n’en reste ». La racine d’agrégat (Slot) garantit cette règle — on ne modifie pas ses Booking enfants directement, on passe par elle.
4. CQRS : séparer lecture et écriture
CQRS (Command Query Responsibility Segregation) sépare :
- les commandes (écriture, « fais X ») → passent par un command bus (Messenger, Partie 12.1), un handler par commande, qui modifie le domaine ;
- les requêtes (lecture, « donne-moi Y ») → passent par un query bus (ou directement des lectures optimisées, DTO — Partie 6.3), sans passer par les entités du domaine.
// Commande (écriture)
final readonly class ConfirmBooking { public function __construct(public int $bookingId) {} }
#[AsMessageHandler]
final class ConfirmBookingHandler { /* charge l'agrégat, applique la règle, persiste */ }
// Requête (lecture) — un DTO léger, pas l'entité
final class BookingListItem { /* id, email, statusLabel */ }Intérêt : le modèle d’écriture (riche, garant des invariants) et le modèle de lecture (plat, optimisé pour l’affichage) sont découplés et évoluent séparément. On peut même, à grande échelle, avoir des bases différentes pour lire et écrire (rare).
5. Structure de dossiers
On matérialise souvent l’hexagonal par une organisation en couches :
src/
├── Domain/ ← entités, value objects, ports (interfaces), événements — ZÉRO Symfony/Doctrine
├── Application/ ← commandes, requêtes, handlers (orchestration)
└── Infrastructure/ ← adapters : Doctrine, HTTP (contrôleurs), Stripe, e-mailLe Domain ne fait aucun use Doctrine\... ni use Symfony\... : il est pur. L’Infrastructure connecte le tout (les repositories Doctrine implémentent les interfaces du domaine).
6. Quand (et quand PAS) appliquer ces patterns
⚠️ Piège de la sur-ingénierie : appliquer DDD/CQRS/hexagonal à un CRUD simple (BookLab au début, un back-office admin) est contre-productif : des couches, des interfaces et des bus pour… déplacer une ligne d’une table à l’autre. Vous multipliez le code et la complexité pour zéro bénéfice. Ces patterns paient sur un domaine réellement complexe (règles métier riches, invariants, plusieurs équipes). Commencez simple (entités + services + contrôleurs fins), et introduisez ces patterns là où la complexité le justifie — pas partout, pas par principe.
Règle : la complexité de l’architecture doit correspondre à la complexité du domaine. Un blog n’a pas besoin de CQRS ; un système de trading, si.
💡 Pour un dev Next.js : c’est la clean architecture que vous connaissez peut-être (séparer la logique métier des frameworks/UI). L’hexagonal côté Symfony ≈ isoler votre logique de React/Next : la logique pure, testable, indépendante de la couche de présentation/persistance. Même sagesse : ne pas over-architecturer un petit projet. La compétence d’expert n’est pas d’appliquer ces patterns partout, mais de savoir où et quand — c’est ce qui vous distingue (Partie 17).
✏️ Exercices
Exercice 1. Dans une architecture hexagonale, de quoi dépend le domaine pour persister un Booking ?
✅ Solution
D’une interface (un port), ex. BookingRepositoryInterface, définie dans le domaine. Le domaine ne connaît pas Doctrine : il manipule cette interface. L’infrastructure fournit un adapter (DoctrineBookingRepository) qui l’implémente. Ainsi le domaine reste pur et testable (on injecte un double du repository), et on peut changer d’implémentation sans toucher la logique métier.
Exercice 2. Qu’est-ce que CQRS sépare, et pourquoi ?
✅ Solution
CQRS sépare les commandes (écriture : « fais X », qui modifient le domaine via un command bus + handlers, en garantissant les invariants) des requêtes (lecture : « donne-moi Y », via des DTO/lectures optimisées). Pourquoi : le modèle d’écriture (riche, garant des règles) et le modèle de lecture (plat, optimisé pour l’affichage) ont des besoins différents ; les découpler permet à chacun d’évoluer et d’être optimisé indépendamment (et évite de charger des agrégats lourds juste pour afficher une liste).
Exercice 3. Un client vous demande un back-office CRUD de 8 entités. Appliquez-vous DDD/CQRS/hexagonal ? Justifiez.
✅ Solution
Non (ou très légèrement). Un CRUD est un domaine simple : entités + services + contrôleurs fins + EasyAdmin suffisent (Parties 6-7). Y appliquer DDD/CQRS/hexagonal ajouterait des couches, interfaces et bus pour zéro bénéfice métier — de la sur-ingénierie coûteuse à écrire et à maintenir. On réserve ces patterns aux domaines riches (règles complexes, invariants). La règle : l’architecture doit correspondre à la complexité du domaine. Savoir dire « non » ici est une marque d’expertise.
🧠 Quiz de révision
1. Quel est le principe de l’architecture hexagonale ?
Le domaine au centre (pur, sans framework), l’infrastructure aux bords, reliés par des ports (interfaces). Le domaine ne dépend pas de Doctrine/Symfony.
2. Citez trois briques DDD.
Au choix : entité, value object, agrégat, repository (interface), événement de domaine, service de domaine.
3. Que sépare CQRS ?
Les commandes (écriture, command bus + handlers) des requêtes (lecture, DTO/lectures optimisées).
4. Comment matérialise-t-on souvent l’hexagonal en dossiers ?
src/Domain (pur), src/Application (commandes/requêtes/handlers), src/Infrastructure (adapters Doctrine/HTTP/…).
5. Quand ne PAS appliquer ces patterns ?
Sur un CRUD/domaine simple : ce serait de la sur-ingénierie. L’architecture doit correspondre à la complexité du domaine.
Prochain chapitre : 15.8 — Créer & publier un bundle — packager et diffuser du code réutilisable.