Chapitre 13
13 — Architectures Microservices, Event-Driven & CQRS
13 — Architectures Microservices, Event-Driven & CQRS
Chapitre 13 : Architectures Microservices, Event-Driven & CQRS
Durée estimée : 5 séances de 3h Objectifs : Comprendre les architectures microservices, l'event-driven design, CQRS, Event Sourcing, Saga Pattern, et savoir quand les utiliser.
Partie 1 : Microservices — Principes Fondamentaux
1.1 Définition
Les microservices sont un style architectural où l'application est composée de petits services indépendants, chacun :
- Responsable d'une capacité métier spécifique
- Déployable indépendamment
- Communiquant via des APIs légères (HTTP/REST, gRPC, messaging)
- Possédant sa propre base de données
1.2 Monolith vs Microservices
Diagramme en cours de génération...
| Critère | Monolith | Microservices |
|---|---|---|
| Déploiement | Un seul artefact | N artefacts indépendants |
| Évolutivité | Scale vertical | Scale horizontal par service |
| Complexité | Initiale faible | Initiale élevée |
| Base de données | Une seule | Une par service |
| Communication | Appels de fonction | HTTP/gRPC/Message Queue |
| Couplage | Fort | Faible |
| Tests | Intégration simples | Tests distribués complexes |
1.3 API Gateway Pattern
class ApiGateway {
private routes: Map<string, string> = new Map();
constructor() {
this.routes.set('/users', 'http://user-service:3001');
this.routes.set('/orders', 'http://order-service:3002');
this.routes.set('/payments', 'http://payment-service:3003');
}
async handleRequest(req: Request): Promise<Response> {
const serviceUrl = this.getServiceUrl(req.url);
if (!serviceUrl) {
return new Response('Not Found', { status: 404 });
}
// Rate limiting, auth, logging
if (!this.checkRateLimit(req)) {
return new Response('Too Many Requests', { status: 429 });
}
// Forward request
const response = await fetch(`${serviceUrl}${req.url}`, {
method: req.method,
headers: req.headers,
body: req.body
});
return response;
}
private getServiceUrl(url: string): string | null {
for (const [prefix, serviceUrl] of this.routes) {
if (url.startsWith(prefix)) return serviceUrl;
}
return null;
}
private checkRateLimit(req: Request): boolean {
// Rate limiting logic per IP
return true;
}
}
Partie 2 : Event-Driven Architecture
2.1 Principe
L'Event-Driven Architecture (EDA) est basée sur la production, la détection et la réaction à des événements. Les services communiquent via des événements asynchrones plutôt que des appels synchrones.
2.2 Types d'événements
// Event interface
interface DomainEvent {
eventId: string;
eventType: string;
aggregateId: string;
timestamp: Date;
data: any;
}
// Concrete events
class OrderCreatedEvent implements DomainEvent {
eventId = crypto.randomUUID();
eventType = 'order.created';
timestamp = new Date();
constructor(
public aggregateId: string,
public data: { userId: string; items: any[]; total: number }
) {}
}
class PaymentCompletedEvent implements DomainEvent {
eventId = crypto.randomUUID();
eventType = 'payment.completed';
timestamp = new Date();
constructor(
public aggregateId: string,
public data: { transactionId: string; amount: number }
) {}
}
class OrderShippedEvent implements DomainEvent {
eventId = crypto.randomUUID();
eventType = 'order.shipped';
timestamp = new Date();
constructor(
public aggregateId: string,
public data: { trackingNumber: string }
) {}
}
2.3 Event Bus Implementation
type EventHandler = (event: DomainEvent) => Promise<void>;
class EventBus {
private handlers: Map<string, Set<EventHandler>> = new Map();
private deadLetterQueue: DomainEvent[] = [];
subscribe(eventType: string, handler: EventHandler): () => void {
if (!this.handlers.has(eventType)) {
this.handlers.set(eventType, new Set());
}
this.handlers.get(eventType)!.add(handler);
return () => this.handlers.get(eventType)?.delete(handler);
}
async publish(event: DomainEvent): Promise<void> {
const handlers = this.handlers.get(event.eventType);
if (!handlers) return;
const promises = Array.from(handlers).map(handler =>
handler(event).catch(err => {
console.error(`Handler failed for ${event.eventType}:`, err);
this.deadLetterQueue.push(event);
})
);
await Promise.all(promises);
}
getDeadLetterQueue(): DomainEvent[] {
return [...this.deadLetterQueue];
}
}
// Usage: Order Service
class OrderService {
constructor(private eventBus: EventBus) {}
async createOrder(userId: string, items: any[]): Promise<string> {
const orderId = crypto.randomUUID();
// Business logic
const order = { id: orderId, userId, items, status: 'created' };
await this.saveOrder(order);
// Publish event
await this.eventBus.publish(new OrderCreatedEvent(orderId, {
userId,
items,
total: items.reduce((s, i) => s + i.price, 0)
}));
return orderId;
}
}
// Usage: Notification Service
class NotificationService {
constructor(private eventBus: EventBus) {
this.eventBus.subscribe('order.created', this.handleOrderCreated.bind(this));
this.eventBus.subscribe('order.shipped', this.handleOrderShipped.bind(this));
}
private async handleOrderCreated(event: DomainEvent): Promise<void> {
console.log(`Sending confirmation email for order ${event.aggregateId}`);
// Send email logic
}
private async handleOrderShipped(event: DomainEvent): Promise<void> {
console.log(`Sending shipping notification for order ${event.aggregateId}`);
// Send push notification
}
}
Partie 3 : CQRS — Command Query Responsibility Segregation
3.1 Principe
CQRS sépare les opérations de lecture (queries) des opérations d'écriture (commands). Chaque côté peut avoir son propre modèle et sa propre base de données.
3.2 Structure
Diagramme en cours de génération...
3.3 Implémentation
// === COMMAND SIDE ===
// Commands
interface Command {
commandId: string;
timestamp: Date;
}
class CreateOrderCommand implements Command {
commandId = crypto.randomUUID();
timestamp = new Date();
constructor(
public userId: string,
public items: Array<{ productId: string; quantity: number; price: number }>
) {}
}
class AddItemCommand implements Command {
commandId = crypto.randomUUID();
timestamp = new Date();
constructor(
public orderId: string,
public productId: string,
public quantity: number,
public price: number
) {}
}
// Command Handler
class OrderCommandHandler {
constructor(
private writeRepo: OrderWriteRepository,
private eventBus: EventBus
) {}
async handle(command: CreateOrderCommand): Promise<string> {
const orderId = crypto.randomUUID();
const order = new OrderAggregate(orderId, command.userId);
for (const item of command.items) {
order.addItem(item.productId, item.quantity, item.price);
}
await this.writeRepo.save(order);
for (const event of order.getUncommittedEvents()) {
await this.eventBus.publish(event);
}
return orderId;
}
}
// Aggregate
class OrderAggregate {
private items: OrderItem[] = [];
private uncommittedEvents: DomainEvent[] = [];
constructor(
public readonly id: string,
public readonly userId: string
) {}
addItem(productId: string, quantity: number, price: number): void {
this.items.push({ productId, quantity, price });
this.uncommittedEvents.push(new OrderItemAddedEvent(this.id, {
productId, quantity, price
}));
}
getUncommittedEvents(): DomainEvent[] {
const events = [...this.uncommittedEvents];
this.uncommittedEvents = [];
return events;
}
}
// === QUERY SIDE ===
class OrderQueryHandler {
constructor(private readRepo: OrderReadRepository) {}
async getOrder(id: string): Promise<OrderDTO | null> {
return this.readRepo.findById(id);
}
async getUserOrders(userId: string): Promise<OrderDTO[]> {
return this.readRepo.findByUserId(userId);
}
async getOrderSummary(userId: string): Promise<OrderSummary> {
const orders = await this.readRepo.findByUserId(userId);
return {
totalOrders: orders.length,
totalSpent: orders.reduce((s, o) => s + o.total, 0),
lastOrder: orders[orders.length - 1]
};
}
}
// Read Model Projection
class OrderProjection {
constructor(private readRepo: OrderReadRepository) {}
async onOrderCreated(event: OrderCreatedEvent): Promise<void> {
await this.readRepo.save({
id: event.aggregateId,
userId: event.data.userId,
items: event.data.items,
total: event.data.total,
status: 'created',
createdAt: event.timestamp
});
}
async onOrderShipped(event: OrderShippedEvent): Promise<void> {
await this.readRepo.updateStatus(event.aggregateId, 'shipped');
}
}
Partie 4 : Event Sourcing
4.1 Principe
Event Sourcing stocke l'état d'un système comme une séquence d'événements, plutôt que l'état actuel. Pour connaître l'état actuel, on rejoue tous les événements.
Diagramme en cours de génération...
4.2 Avantages et Inconvénients
| Avantages | Inconvénients |
|---|---|
| Audit trail complet | Complexité élevée |
| Time travel debugging | Stockage volumineux |
| Rejeu d'événements | Event versioning |
| Opportunités d'analyse (event mining) | Performance des rejeux |
Partie 5 : Saga Pattern
5.1 Principe
Saga gère les transactions distribuées en les décomposant en une série de transactions locales avec des compensations (rollback).
5.2 Choreography-based Saga
Diagramme en cours de génération...
5.3 Orchestration-based Saga
class SagaOrchestrator {
async executeCreateOrderSaga(input: CreateOrderInput): Promise<void> {
const sagaId = crypto.randomUUID();
try {
// Step 1: Reserve inventory
const inventoryResult = await this.inventoryService.reserve(input.items);
// Step 2: Process payment
const paymentResult = await this.paymentService.charge(input.userId, input.total);
// Step 3: Create order
const order = await this.orderService.create(input);
// Step 4: Confirm
await this.orderService.confirm(order.id);
} catch (err) {
// Compensating transactions
await this.inventoryService.release(input.items);
await this.paymentService.refund(input.userId, input.total);
await this.orderService.cancel(sagaId);
throw new Error('Saga failed, compensating transactions executed');
}
}
}
Partie 6 : Strangler Fig Pattern
Migration progressive du monolithe vers les microservices :
Diagramme en cours de génération...
Résumé
| Pattern | Quand l'utiliser | Risques |
|---|---|---|
| Microservices | App complexe, équipes multiples, scale indépendant | Complexité distribuée |
| Event-Driven | Découplage, async, réactivité | Eventual consistency, debugging |
| CQRS | Charges lecture/écriture différentes | Complexité, duplication |
| Event Sourcing | Audit trail, time travel | Stockage, performance |
| Saga | Transactions distribuées | Complexité des compensations |
| Strangler Fig | Migration monolithe→microservices | Période de coexistence |
Prochain chapitre : Concurrency Patterns (Active Object, Reactor, Thread Pool).