// Architecture Réactive — Gregor Hohpe · Greg Young · Uncle Bob
EDA · CQRS · SAGA NestJS
Le triptyque des architectures réactives modernes — Event-Driven Architecture, Command Query Responsibility Segregation, Pattern SAGA, intégrés dans NestJS avec TypeScript.
EDA, CQRS et SAGA ne sont pas trois architectures alternatives — ce sont trois couches complémentaires qui s’emboîtent. L’EDA pose le canal de communication (les événements). Le CQRS sépare les responsabilités lecture/écriture. Le SAGA orchestre les transactions distribuées sur ce canal.
Couche 1 — Communication
EDA
Les services communiquent via des événements immuables. Couplage minimal. Producteurs et consommateurs s’ignorent.
Couche 2 — Organisation
CQRS
Les commandes mutent l’état. Les queries lisent des vues optimisées. Jamais les deux en même temps.
Couche 3 — Coordination
SAGA
Transactions distribuées via des séquences d’événements avec compensation en cas d’échec. Pas de 2PC global.
EDA — Producteurs, Event Bus, Consommateurs
02
Métaphores
📻
EDA — La Station de Radio
L’émetteur diffuse dans l’éther sans savoir combien d’auditeurs l’écoutent. Les auditeurs s’abonnent librement aux fréquences qui les intéressent. Personne n’attend personne. Un événement est un fait passé immuable : “La commande #42 a été passée”, jamais “Passe la commande #42”.
👨🍳
CQRS — Le Restaurant Étoilé
La brigade de cuisine (Write Side) reçoit les bons de commande, transforme et modifie l’état. Les serveurs en salle (Read Side) lisent le tableau des plats et servent. La cuisine ne parle jamais directement aux clients. Les serveurs ne touchent jamais aux fourneaux. Le tableau des plats du jour est une projection dénormalisée mise à jour dès qu’un plat change.
✈️
SAGA — L’Agent de Voyage
Tu réserves vol + hôtel + voiture : trois entreprises différentes. Impossible de faire un BEGIN TRANSACTION global. L’agent de voyage coordonne séquentiellement : vol ✅ → hôtel ✅ → voiture ❌ → annuler l’hôtel → annuler le vol. Chaque étape est une transaction locale. Chaque échec déclenche des actions de compensation explicites en sens inverse.
03
EDA — Concepts Fondamentaux
📤
Producteur
Publisher / Emitter
Publie un evenement sans savoir qui l'ecoutera. Ne retourne rien. Ne connait aucun consommateur. Couplage zero vers l'aval.
fire & forgetno return
🚌
Event Bus
Message Broker
Canal de transport. Garantit livraison, routage par topic, persistance. C'est un detail d'infrastructure — derriere une interface.
KafkaRabbitMQNATS
📥
Consommateur
Subscriber / Listener
S'abonne aux evenements qui l'interessent. Reagit de maniere autonome. Plusieurs consommateurs peuvent ecouter le meme evenement.
subscribeidempotent
Structure d’un Événement
Règle fondamentale : Un événement est un fait passé, immuable, daté. Il décrit ce qui s’est passé — pas ce qui doit se passer. Nommez-le au passé composé : OrderCreated, PaymentCompleted, jamais CreateOrder.
domain-event.interface.ts
// Interface de base — tout evenement respecte ce contratinterfaceDomainEvent {
readonly eventId: string; // UUID — identifiant unique de l'eventreadonly eventType: string; // 'order.created' — discriminantreadonly aggregateId: string; // ID de l'entite concerneereadonly occurredAt: Date; // Quand c'est arrive (passe)readonly version: number; // Pour l'ordering et la concurrencereadonly payload: Record<string, unknown>;
}
// Evenement concret — typeclassOrderCreatedEvent {
constructor(
public readonly orderId: string,
public readonly customerId: string,
public readonly items: ReadonlyArray<{ productId: string; quantity: number; price: number }>,
public readonly totalAmount: number,
public readonly occurredAt: Date = newDate(),
) {}
}
04
EDA — Les 3 Patterns
🔔
Pattern 1
Simple Event Notification
Fire & forget. Le producteur emet, les consommateurs reagissent. Decouplage maximal. Ideal pour les notifications et les side-effects non-critiques.
AsyncDecoupleSimple
📚
Pattern 2
Event Sourcing
L'etat de l'application = rejouer la sequence des evenements. L'Event Store est la source de verite. Audit trail parfait. Time travel possible.
Append-onlyReplayAudit
⚡
Pattern 3
CQRS + Event Sourcing
Write Side base sur Event Sourcing. Read Side base sur des projections denormalisees reconstruites depuis les events. Scalabilite R/W independante.
ScalableEventualOptimise
Event Bus — Implémentation TypeScript
in-memory-event-bus.ts
typeHandler<T> = (event: T) => Promise<void>;
interfaceIEventBus {
publish<TextendsDomainEvent>(event: T): Promise<void>;
subscribe<TextendsDomainEvent>(eventType: string, handler: Handler<T>): void;
}
// In-Memory — pour les tests et le monolitheclassInMemoryEventBusimplementsIEventBus {
private readonly handlers = newMap<string, Set<Handler<any>>>();
subscribe<T>(eventType: string, handler: Handler<T>): void {
if (!this.handlers.has(eventType))
this.handlers.set(eventType, newSet());
this.handlers.get(eventType)!.add(handler);
}
asyncpublish<TextendsDomainEvent>(event: T): Promise<void> {
const handlers = this.handlers.get(event.eventType) ?? newSet();
// Parallel — les consommateurs sont independantsawaitPromise.allSettled([...handlers].map(h => h(event)));
}
}
05
CQRS — Séparation Read / Write
CQRS applique à l’architecture ce qu’Uncle Bob applique aux fonctions dans Clean Code : une fonction fait UNE chose — soit elle répond à une question, soit elle exécute une action, jamais les deux. CQRS étend ce principe à l’ensemble du système.
COMMAND — Intention de changer l’état. Retourne void ou un ID. Valide. Produit des Events. QUERY — Demande de lire l’état. Retourne un DTO. Ne modifie JAMAIS l’état. Fonction pure. PROJECTION — Consomme des Events. Maintient une vue READ optimisée. Idempotente. Rejouable.
CQRS — Write Side, Projections, Read Side
Projections — Le cycle de vie
Super-pouvoir du Replay : Si une projection est corrompue ou si une nouvelle vue est nécessaire, on relit l’Event Store depuis le début. Les events étant immuables, le replay produit toujours exactement le même résultat. C’est le Time Travel gratuit de l’Event Sourcing.
Le pattern SAGA résout le problème des transactions qui s’étendent sur plusieurs services. Il n’existe pas de BEGIN TRANSACTION global en distribué. La SAGA remplace l’atomicité ACID par une séquence d’étapes locales, chacune réversible via une action de compensation.
Idempotence — Non Négociable : Les brokers garantissent at-least-once delivery. Ton service de paiement peut recevoir le même message deux fois. Sans idempotence, tu débites deux fois le client. Solution : stocker le sagaId comme clé unique de déduplication.
Choreography vs Orchestration
💃
Style 1
Choreography
Chaque service connait ses voisins via des evenements. Pas de coordinateur central. Simple a mettre en place.
✓ Pas de SPOF✓ Simple✗ Logique dispersee✗ Debug difficile
🎼
Style 2
Orchestration
Un Orchestrateur central coordonne tous les services. Logique centralisee, lisible, monitorable. Services participants restent simples.
✓ Lisible✓ Monitorable✗ SPOF potentiel✗ Couplage central
@nestjs/cqrs fournit trois bus (CommandBus, QueryBus, EventBus) et quatre décorateurs (@CommandHandler, @QueryHandler, @EventsHandler, @Saga) qui s’injectent via le DI de NestJS. CqrsModule.forRoot() câble tout automatiquement.
Le décorateur @Saga() de NestJS transforme un flux d’événements (Observable) en flux de commandes (Observable<ICommand>). C’est l’intégration native du pattern Choreography avec RxJS.
Ne jamais laisser l’Observable mourir : Si le pipeline RxJS lance une exception non catchée, le SAGA cesse de fonctionner silencieusement. Toujours encadrer avec catchError((err, src) => { logger.error(err); return src; }) pour relancer la source.
Publier avant de persister.mergeObjectContext(aggregate) publie les events immediatement dans apply(). Si le save() echoue ensuite, les projections sont corrompues et l'etat est incoherent. Regle : persister d'abord, publishAll() ensuite.
!
Observable SAGA sans catchError. Une exception non catchee termine l'Observable silencieusement. Le SAGA cesse de fonctionner sans aucun log d'erreur. Toujours : catchError((err, src) => { logger.error(err); return src; }).
!
EventBus synchrone dans le meme process. Par defaut, EventBus.publish() en NestJS est synchrone dans le meme processus. Pour une vraie EDA distribuee (Kafka, RabbitMQ), brancher un publisher custom via @nestjs/microservices ou un adaptateur externe.
!
Logique metier dans l'Orchestrateur. L'orchestrateur SAGA dirige quoi faire, pas comment. Des qu'il contient une regle metier (calcul, condition autre que le statut), c'est une violation. La regle va dans l'Agregat, pas dans la SAGA.
!
@Entity() TypeORM sur l'Agregat. La violation la plus repandue en NestJS. L'Agregat appartient au domaine — zero dependance vers @nestjs/typeorm. Utiliser le double modele : Agregat de domaine + ORM Entity + Mapper.
Dans Architecture Logicielle Propre, Uncle Bob insiste sur la Dependency Rule : les dépendances pointent vers l’intérieur. Dans ce triptyque : le domaine (Agrégat) ne dépend de rien. Le Use Case (CommandHandler) dépend du domaine. L’infrastructure (TypeORM, Kafka) dépend du Use Case. Et le bus est l’abstraction frontière qui traverse tout — le producteur ne connaît jamais le consommateur.