Chapitre 17
17. Integration Architecture Patterns
17. Integration Architecture Patterns
Integration Architecture Patterns - Cours Détaillé
Introduction
L'architecture d'intégration traite de la façon dont les différents services et systèmes communiquent entre eux. Dans un environnement distribué, les problèmes de couplage, de résilience, de cohérence des données, et de migration deviennent centraux. Ce chapitre présente les patterns éprouvés pour résoudre ces défis.
1. API Gateway
Définition
Un API Gateway est un point d'entrée unique qui agit comme proxy inverse, acceptant les appels API, les routant vers les services appropriés, et agrégeant les réponses.
Responsabilités
- Routage des requêtes
- Agrégation de réponses
- Authentification / Autorisation
- Rate limiting
- Load balancing
- Transformation de protocole
- Cache
- Logging / Monitoring
Implémentation
// Express Gateway simplifié
class ApiGateway {
private routes: Map<string, RouteConfig> = new Map();
private rateLimiter: RateLimiter;
private authService: AuthService;
constructor() {
this.rateLimiter = new RateLimiter({ windowMs: 60000, max: 100 });
}
registerRoute(path: string, config: RouteConfig): void {
this.routes.set(path, config);
}
async handleRequest(req: Request, res: Response): Promise<void> {
try {
// 1. Rate limiting
await this.rateLimiter.check(req.ip);
// 2. Authentication
const user = await this.authService.authenticate(req);
if (!user) {
res.status(401).json({ error: 'Unauthorized' });
return;
}
// 3. Route matching
const route = this.matchRoute(req.path);
if (!route) {
res.status(404).json({ error: 'Not found' });
return;
}
// 4. Authorization
if (!this.checkPermissions(user, route)) {
res.status(403).json({ error: 'Forbidden' });
return;
}
// 5. Route to service
const response = await this.routeToService(route, req);
// 6. Response transformation
const transformed = this.transformResponse(response, route);
res.json(transformed);
} catch (error) {
this.handleError(error, res);
}
}
private async routeToService(route: RouteConfig, req: Request): Promise<any> {
const startTime = Date.now();
try {
const response = await axios({
method: req.method,
url: `${route.serviceUrl}${req.path}`,
data: req.body,
headers: this.filterHeaders(req.headers),
timeout: route.timeout || 5000,
});
this.recordMetrics(route, Date.now() - startTime, 200);
return response.data;
} catch (error) {
this.recordMetrics(route, Date.now() - startTime, error.response?.status || 500);
throw error;
}
}
}
API Gateway vs BFF
| Critère | API Gateway | BFF |
|---|---|---|
| Point d'entrée | Unique | Un par client |
| Audience | Tous les clients | Client spécifique |
| Complexité | Élevée | Faible |
| Équipe | Plateforme | Feature team |
| Couplage | Centralisé | Distribué |
2. Backend for Frontend (BFF)
Définition
Le pattern BFF consiste à créer un backend dédié pour chaque type de client (mobile, web, IoT), optimisant les APIs pour les besoins spécifiques de chaque client.
Pourquoi BFF ?
// BFF Mobile : API légère pour bande passante limitée
class MobileBFF {
async getProductFeed(userId: string): Promise<MobileProductFeed> {
const [products, preferences, location] = await Promise.all([
productService.getProducts(),
userService.getPreferences(userId),
locationService.getCurrentLocation(userId),
]);
// Agrégation spécifique mobile
return {
items: products
.filter(p => this.isRelevant(p, preferences))
.slice(0, 20) // Pagination mobile
.map(p => ({
id: p.id,
name: p.name,
price: p.price,
thumbnail: p.images[0], // Une seule image
inStock: p.stock > 0,
})),
totalCount: 20,
nextCursor: this.encodeCursor(products[19]?.id),
};
}
}
// BFF Web : API plus riche pour desktop
class WebBFF {
async getProductFeed(userId: string, page: number, pageSize: number): Promise<WebProductFeed> {
const [products, categories] = await Promise.all([
productService.getProductsPaginated(page, pageSize),
categoryService.getAllCategories(),
]);
// Agrégation riche pour le web
return {
items: products.items.map(p => ({
id: p.id,
name: p.name,
description: p.description,
price: p.price,
originalPrice: p.originalPrice,
discount: p.discount,
images: p.images,
variants: p.variants,
specs: p.specifications,
rating: p.averageRating,
reviewCount: p.reviewCount,
inStock: p.stock > 0,
category: categories.find(c => c.id === p.categoryId),
relatedProducts: p.relatedIds,
})),
pagination: {
page,
pageSize,
totalItems: products.total,
totalPages: Math.ceil(products.total / pageSize),
},
};
}
}
Cas d'usage
- Mobile (bande passante limitée, écran small)
- Web (richesse, desktop)
- IoT (protocoles légers)
- Third-party (API publique dédiée)
3. Circuit Breaker
Définition
Le Circuit Breaker est un pattern de résilience qui détecte les défaillances et empêche les appels répétés à un service défaillant, donnant le temps au service de récupérer.
États
[CLOSED] → (failures > threshold) → [OPEN]
[OPEN] → (timeout elapsed) → [HALF_OPEN]
[HALF_OPEN] → (success) → [CLOSED]
[HALF_OPEN] → (failure) → [OPEN]
Implémentation (TypeScript)
type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN';
class CircuitBreaker {
private state: CircuitState = 'CLOSED';
private failureCount = 0;
private successCount = 0;
private lastFailureTime: number = 0;
private readonly failureThreshold: number;
private readonly successThreshold: number;
private readonly timeout: number;
constructor(
private readonly serviceName: string,
options?: Partial<CircuitBreakerOptions>
) {
this.failureThreshold = options?.failureThreshold ?? 5;
this.successThreshold = options?.successThreshold ?? 3;
this.timeout = options?.timeout ?? 30000;
}
async call<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === 'OPEN') {
if (Date.now() - this.lastFailureTime >= this.timeout) {
this.state = 'HALF_OPEN';
} else {
throw new CircuitBreakerOpenError(this.serviceName);
}
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess(): void {
this.failureCount = 0;
if (this.state === 'HALF_OPEN') {
this.successCount++;
if (this.successCount >= this.successThreshold) {
this.state = 'CLOSED';
this.successCount = 0;
}
}
}
private onFailure(): void {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.failureThreshold) {
this.state = 'OPEN';
console.warn(`Circuit breaker OPEN for ${this.serviceName}`);
}
}
getState(): CircuitState { return this.state; }
}
// Resilience4j-like decorator
function circuitBreaker(options?: Partial<CircuitBreakerOptions>) {
const breakers = new Map<string, CircuitBreaker>();
return function <T>(
target: any,
propertyKey: string,
descriptor: TypedPropertyDescriptor<(...args: any[]) => Promise<T>>
) {
const originalMethod = descriptor.value!;
const serviceName = `${target.constructor.name}.${propertyKey}`;
if (!breakers.has(serviceName)) {
breakers.set(serviceName, new CircuitBreaker(serviceName, options));
}
descriptor.value = async function (...args: any[]): Promise<T> {
const breaker = breakers.get(serviceName)!;
return breaker.call(() => originalMethod.apply(this, args));
};
};
}
Métriques et monitoring
interface CircuitBreakerMetrics {
state: CircuitState;
failureCount: number;
successCount: number;
failureRate: number;
lastFailure: Date | null;
uptime: number;
}
class MonitoredCircuitBreaker extends CircuitBreaker {
private metrics: CircuitBreakerMetrics = {
state: 'CLOSED',
failureCount: 0,
successCount: 0,
failureRate: 0,
lastFailure: null,
uptime: 0,
};
getMetrics(): CircuitBreakerMetrics {
return { ...this.metrics };
}
}
4. Saga Pattern
Définition
Le pattern Saga gère les transactions distribuées en les décomposant en une séquence de transactions locales, avec des actions de compensation en cas d'échec.
Choreography Saga
// Choreography : chaque service publie des événements et réagit aux événements
interface OrderEvent {
type: 'OrderCreated' | 'OrderConfirmed' | 'OrderCancelled';
orderId: string;
data: any;
}
// Order Service
class OrderService {
async createOrder(order: OrderInput): Promise<void> {
const orderId = uuid();
await this.orderRepo.save({ id: orderId, ...order, status: 'PENDING' });
await this.eventBus.publish({
type: 'OrderCreated',
orderId,
data: { items: order.items, total: order.total },
});
}
@Subscribe('PaymentProcessed')
async onPaymentProcessed(event: OrderEvent): Promise<void> {
await this.orderRepo.update(event.orderId, { status: 'CONFIRMED' });
await this.eventBus.publish({
type: 'OrderConfirmed',
orderId: event.orderId,
data: {},
});
}
@Subscribe('PaymentFailed')
async onPaymentFailed(event: OrderEvent): Promise<void> {
await this.orderRepo.update(event.orderId, { status: 'CANCELLED' });
// Compensation : annuler la réservation de stock
}
}
// Inventory Service
class InventoryService {
@Subscribe('OrderCreated')
async onOrderCreated(event: OrderEvent): Promise<void> {
try {
await this.reserveStock(event.data.items);
await this.eventBus.publish({
type: 'StockReserved',
orderId: event.orderId,
data: {},
});
} catch (error) {
await this.eventBus.publish({
type: 'StockReservationFailed',
orderId: event.orderId,
data: { reason: error.message },
});
}
}
@Subscribe('OrderCancelled')
async onOrderCancelled(event: OrderEvent): Promise<void> {
await this.releaseStock(event.data.items); // Compensation
}
}
Orchestration Saga
// Orchestrator : un coordinateur central gère la saga
class OrderSagaOrchestrator {
async executeSaga(orderInput: OrderInput): Promise<void> {
const sagaId = uuid();
const context = new SagaContext(sagaId);
try {
// Étape 1 : Créer la commande
const order = await this.orderService.createOrder(orderInput);
context.set('orderId', order.id);
// Étape 2 : Réserver le stock
const stock = await this.inventoryService.reserveStock(orderInput.items);
context.set('reservationId', stock.reservationId);
// Étape 3 : Traiter le paiement
const payment = await this.paymentService.processPayment(orderInput.total);
context.set('paymentId', payment.id);
// Étape 4 : Confirmer la commande
await this.orderService.confirmOrder(order.id);
// Étape 5 : Envoyer la notification
await this.notificationService.sendOrderConfirmation(order.id);
} catch (error) {
await this.compensate(context, error);
}
}
private async compensate(context: SagaContext, error: Error): Promise<void> {
console.error(`Saga ${context.sagaId} failed:`, error);
// Compensation inversée
if (context.has('paymentId')) {
await this.paymentService.refund(context.get('paymentId'));
}
if (context.has('reservationId')) {
await this.inventoryService.releaseStock(context.get('reservationId'));
}
if (context.has('orderId')) {
await this.orderService.cancelOrder(context.get('orderId'));
}
}
}
Choreography vs Orchestration
| Critère | Choreography | Orchestration |
|---|---|---|
| Couplage | Faible (événements) | Plus fort (orchestrator) |
| Visibilité | Diffuse | Centralisée |
| Complexité | Élevée (debug) | Moyenne |
| Scalabilité | Excellente | Limitée (orchestrator) |
| Transitions | Complexes | Simples |
| Responsabilité | Chaque service | Orchestrator |
5. Strangler Fig Pattern
Définition
Le pattern Strangler Fig permet de migrer progressivement un système monolithique vers des microservices en remplaçant des fonctionnalités une par une.
Stratégie
class StranglerProxy {
private legacyBaseUrl: string;
private newServiceRoutes: Map<string, string>;
constructor(legacyBaseUrl: string) {
this.legacyBaseUrl = legacyBaseUrl;
this.newServiceRoutes = new Map();
}
migrateRoute(path: string, newServiceUrl: string): void {
this.newServiceRoutes.set(path, newServiceUrl);
}
async handleRequest(req: Request): Promise<Response> {
const newServiceUrl = this.newServiceRoutes.get(req.path);
if (newServiceUrl) {
// Route vers le nouveau service
try {
const response = await axios.post(newServiceUrl, req.body);
return response;
} catch (error) {
// Fallback vers le legacy si le nouveau service échoue
console.warn(`Falling back to legacy for ${req.path}`);
return this.forwardToLegacy(req);
}
}
// Route vers le legacy par défaut
return this.forwardToLegacy(req);
}
private async forwardToLegacy(req: Request): Promise<Response> {
return axios({
method: req.method,
url: `${this.legacyBaseUrl}${req.path}`,
data: req.body,
});
}
}
Phases de migration
// Phase 1 : Proxy initial
const proxy = new StranglerProxy('https://legacy-app.com/api');
// Phase 2 : Migrer une route
proxy.migrateRoute('/api/users', 'https://users-service.com/api');
// Phase 3 : Migrer plus de routes
proxy.migrateRoute('/api/products', 'https://products-service.com/api');
proxy.migrateRoute('/api/orders', 'https://orders-service.com/api');
// Phase 4 : Supprimer le proxy quand la migration est terminée
6. Sidecar Pattern
Définition
Le Sidecar est un conteneur auxiliaire attaché à l'application principale, partageant le même cycle de vie et les mêmes ressources.
Cas d'utilisation
- Proxy / Load balancing (Envoy, Linkerd)
- Logging collecteur (Fluentd)
- Monitoring (Prometheus exporter)
- Configuration reload
- TLS termination
- Authentication proxy
Configuration Docker Compose
version: '3.8'
services:
app:
image: my-app:latest
ports:
- "3000:3000"
depends_on:
- sidecar
sidecar:
image: envoyproxy/envoy:v1.28
volumes:
- ./envoy.yaml:/etc/envoy/envoy.yaml
ports:
- "9901:9901" # Admin
network_mode: "service:app"
Kubernetes Sidecar
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-with-sidecar
spec:
replicas: 3
template:
spec:
containers:
- name: main-app
image: my-app:latest
ports:
- containerPort: 3000
- name: sidecar-proxy
image: envoyproxy/envoy:v1.28
ports:
- containerPort: 9901
volumeMounts:
- name: envoy-config
mountPath: /etc/envoy
volumes:
- name: envoy-config
configMap:
name: envoy-config
7. Ambassador Pattern
Définition
L'Ambassador est un proxy qui se place entre le client et les services externes pour gérer la connectivité, la résilience, et la sécurité.
Différence avec Sidecar
- Sidecar : Compagnon local, même pod/host
- Ambassador : Proxy dédié, souvent séparé
class Ambassador {
private circuitBreaker: CircuitBreaker;
private retryPolicy: RetryPolicy;
private cache: Cache;
constructor(private serviceUrl: string) {
this.circuitBreaker = new CircuitBreaker(serviceUrl);
this.retryPolicy = new RetryPolicy({ maxRetries: 3, backoff: 'exponential' });
this.cache = new Cache({ ttl: 300000 }); // 5 min
}
async call<T>(request: AmbassadorRequest): Promise<T> {
const cacheKey = this.buildCacheKey(request);
const cached = this.cache.get(cacheKey);
if (cached) return cached as T;
return this.circuitBreaker.call(async () => {
return this.retryPolicy.execute(async () => {
const response = await axios({
method: request.method,
url: `${this.serviceUrl}${request.path}`,
data: request.body,
headers: {
'X-Request-Id': uuid(),
'X-Ambassador-Version': '1.0',
...request.headers,
},
timeout: request.timeout || 5000,
});
if (request.cacheable) {
this.cache.set(cacheKey, response.data);
}
return response.data;
});
});
}
}
8. Anti-Corruption Layer (ACL)
Définition
L'Anti-Corruption Layer est une couche de traduction entre deux systèmes qui empêche la propagation des modèles de données et des concepts d'un système à l'autre.
// Legacy System Model
interface LegacyCustomer {
CUST_ID: string;
CUST_NAME: string;
CUST_EMAIL: string;
CUST_ADDR1: string;
CUST_ADDR2: string;
CUST_CITY: string;
CUST_ZIP: string;
CUST_CREDIT_LIMIT: number;
}
// New System Model
interface Customer {
id: string;
name: string;
email: string;
address: Address;
creditLimit: CreditLimit;
}
interface Address {
street: string;
complement?: string;
city: string;
zipCode: string;
country: string;
}
// Anti-Corruption Layer
class LegacyCustomerTranslator {
// Mapping : Legacy → New
toNew(legacy: LegacyCustomer): Customer {
return {
id: legacy.CUST_ID,
name: legacy.CUST_NAME.trim(),
email: legacy.CUST_EMAIL.toLowerCase(),
address: {
street: [legacy.CUST_ADDR1, legacy.CUST_ADDR2]
.filter(Boolean)
.join(', '),
city: legacy.CUST_CITY,
zipCode: legacy.CUST_ZIP,
country: 'FR',
},
creditLimit: {
amount: legacy.CUST_CREDIT_LIMIT,
currency: 'EUR',
},
};
}
// Mapping : New → Legacy
toLegacy(customer: Customer): LegacyCustomer {
const [addr1, addr2 = ''] = customer.address.street.split(', ');
return {
CUST_ID: customer.id,
CUST_NAME: customer.name.toUpperCase(),
CUST_EMAIL: customer.email,
CUST_ADDR1: addr1,
CUST_ADDR2: addr2,
CUST_CITY: customer.address.city,
CUST_ZIP: customer.address.zipCode,
CUST_CREDIT_LIMIT: customer.creditLimit.amount,
};
}
}
// ACL Facade
class LegacyCustomerService {
private translator = new LegacyCustomerTranslator();
private legacyApi = new LegacySoapClient();
async getCustomer(id: string): Promise<Customer> {
const legacyCustomer = await this.legacyApi.getCustomer(id);
return this.translator.toNew(legacyCustomer);
}
async saveCustomer(customer: Customer): Promise<void> {
const legacyCustomer = this.translator.toLegacy(customer);
await this.legacyApi.saveCustomer(legacyCustomer);
}
}
9. Domain Event
Définition
Un Domain Event capture quelque chose qui s'est passé dans le domaine, permettant aux autres services de réagir de manière découplée.
// Base Domain Event
interface DomainEvent {
eventId: string;
aggregateId: string;
eventType: string;
occurredOn: Date;
version: number;
}
// Événements spécifiques
class OrderPlacedEvent implements DomainEvent {
readonly eventId: string = uuid();
readonly occurredOn: Date = new Date();
readonly version: number = 1;
constructor(
readonly aggregateId: string,
readonly customerId: string,
readonly items: OrderItem[],
readonly total: Money
) {}
readonly eventType = 'order.placed';
}
class PaymentReceivedEvent implements DomainEvent {
readonly eventId: string = uuid();
readonly occurredOn: Date = new Date();
readonly version: number = 1;
constructor(
readonly aggregateId: string,
readonly paymentId: string,
readonly amount: Money,
readonly method: PaymentMethod
) {}
readonly eventType = 'payment.received';
}
// Event Bus
interface EventBus {
publish(event: DomainEvent): Promise<void>;
subscribe(eventType: string, handler: EventHandler): void;
}
// Event Store (pour traçabilité)
class EventStore {
private events: DomainEvent[] = [];
async append(event: DomainEvent): Promise<void> {
this.events.push(event);
await this.persist(event);
}
async getEvents(aggregateId: string): Promise<DomainEvent[]> {
return this.events.filter(e => e.aggregateId === aggregateId);
}
async replay(aggregateId: string): Promise<void> {
const events = await this.getEvents(aggregateId);
for (const event of events) {
await this.notifyHandlers(event);
}
}
}
10. Outbox Pattern
Définition
L'Outbox Pattern garantit la livraison fiable des événements en stockant d'abord les événements dans une table de sortie (outbox) avec la transaction métier, puis en les publiant de manière asynchrone.
Problème résolu
[Service A] → DB (transaction OK) → Message Broker → [Service B]
↓
Crash avant envoi ! → Message perdu
Solution Outbox
// Outbox Repository
interface OutboxMessage {
id: string;
aggregateType: string;
aggregateId: string;
eventType: string;
payload: string; // JSON serialized event
status: 'PENDING' | 'PUBLISHED' | 'FAILED';
createdAt: Date;
publishedAt?: Date;
retryCount: number;
}
// Transactional Outbox
class OutboxService {
constructor(
private db: Database,
private eventBus: EventBus
) {}
async saveAndPublish<T extends DomainEvent>(
aggregateId: string,
event: T,
saveAction: () => Promise<void>
): Promise<void> {
const outboxMessage: OutboxMessage = {
id: uuid(),
aggregateType: event.constructor.name,
aggregateId,
eventType: event.eventType,
payload: JSON.stringify(event),
status: 'PENDING',
createdAt: new Date(),
retryCount: 0,
};
// Même transaction métier + outbox
await this.db.transaction(async (tx) => {
await saveAction(); // 1. Business logic
await tx.save(outboxMessage); // 2. Outbox message
});
// Publication asynchrone
await this.publishMessage(outboxMessage);
}
private async publishMessage(message: OutboxMessage): Promise<void> {
try {
const event = JSON.parse(message.payload);
await this.eventBus.publish(event);
await this.db.update(message.id, {
status: 'PUBLISHED',
publishedAt: new Date(),
});
} catch (error) {
await this.db.update(message.id, {
status: 'FAILED',
retryCount: message.retryCount + 1,
});
}
}
// Relance des messages échoués
async retryFailedMessages(): Promise<void> {
const failedMessages = await this.db.find({
status: 'FAILED',
retryCount: { $lt: 5 },
createdAt: { $gt: new Date(Date.now() - 24 * 60 * 60 * 1000) },
});
for (const message of failedMessages) {
await this.publishMessage(message);
}
}
}
// Usage
class OrderService {
constructor(private outbox: OutboxService) {}
async placeOrder(orderInput: OrderInput): Promise<Order> {
const order = new Order(orderInput);
const event = new OrderPlacedEvent(order.id, orderInput.customerId, orderInput.items, orderInput.total);
await this.outbox.saveAndPublish(
order.id,
event,
async () => {
await this.orderRepository.save(order);
await this.inventoryRepository.reserve(orderInput.items);
}
);
return order;
}
}
Polling Publisher
class OutboxPublisher {
private running = false;
constructor(
private db: Database,
private eventBus: EventBus,
private pollInterval: number = 1000 // 1 seconde
) {}
async start(): Promise<void> {
this.running = true;
while (this.running) {
const messages = await this.db.find({
status: 'PENDING',
createdAt: { $lt: new Date() },
});
for (const message of messages) {
try {
const event = JSON.parse(message.payload);
await this.eventBus.publish(event);
await this.db.update(message.id, { status: 'PUBLISHED', publishedAt: new Date() });
} catch (error) {
console.error(`Failed to publish message ${message.id}:`, error);
await this.db.update(message.id, { status: 'FAILED', retryCount: message.retryCount + 1 });
}
}
await sleep(this.pollInterval);
}
}
async stop(): Promise<void> {
this.running = false;
}
}
11. Résumé et comparaison
| Pattern | Problème résolu | Force | Faiblesse |
|---|---|---|---|
| API Gateway | Point d'entrée unique | Centralisation | SPOF, complexité |
| BFF | API adaptée au client | Performance, découplage | Duplication |
| Circuit Breaker | Résilience | Protection en cascade | Complexité ajoutée |
| Saga | Transactions distribuées | Cohérence sans 2PC | Compensation complexe |
| Strangler Fig | Migration progressive | Faible risque | Longue durée |
| Sidecar | Fonctionnalités auxiliaires | Découplage, polyglotte | Consommation ressources |
| Ambassador | Connectivité externe | Résilience, sécurité | Point de défaillance |
| ACL | Protection modèle | Isolation | Maintenance traduction |
| Domain Event | Découplage temporel | Scalabilité | Debug complexe |
| Outbox | Livraison fiable | Garantie "at least once" | Stockage supplémentaire |
12. Conclusion
Les patterns d'intégration sont essentiels dans les architectures modernes. Le choix du bon pattern dépend du contexte, des contraintes, et de l'architecture existante. L'important est de comprendre chaque pattern, ses forces et ses faiblesses, pour faire le bon compromis.