MFormations
Modern Design Patterns

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èreMonolithMicroservices
DéploiementUn seul artefactN artefacts indépendants
ÉvolutivitéScale verticalScale horizontal par service
ComplexitéInitiale faibleInitiale élevée
Base de donnéesUne seuleUne par service
CommunicationAppels de fonctionHTTP/gRPC/Message Queue
CouplageFortFaible
TestsIntégration simplesTests 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

AvantagesInconvénients
Audit trail completComplexité élevée
Time travel debuggingStockage volumineux
Rejeu d'événementsEvent 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é

PatternQuand l'utiliserRisques
MicroservicesApp complexe, équipes multiples, scale indépendantComplexité distribuée
Event-DrivenDécouplage, async, réactivitéEventual consistency, debugging
CQRSCharges lecture/écriture différentesComplexité, duplication
Event SourcingAudit trail, time travelStockage, performance
SagaTransactions distribuéesComplexité des compensations
Strangler FigMigration monolithe→microservicesPériode de coexistence

Prochain chapitre : Concurrency Patterns (Active Object, Reactor, Thread Pool).