Architecture Logicielle · Patterns Enterprise

Service-Oriented
Architecture

Concevoir des systèmes d’information comme un réseau de services autonomes, interopérables et réutilisables — gouvernés par des contrats formels.

01

La Métaphore

🏛️

La Ville et ses Administrations

Imagine une ville avec ses services publics : la mairie, la bibliothèque, la poste, la banque, l’hôpital. Chaque institution est autonome, a ses propres règles internes, mais elles communiquent toutes via un langage commun — formulaires standardisés, protocoles officiels.

Quand tu veux construire une maison, tu passes par un guichet central (l’ESB) qui contacte le cadastre, la mairie, le fisc. Chaque service répond avec un contrat formel (WSDL ou OpenAPI). Tu reçois une réponse consolidée. C’est exactement ce qu’est la SOA.

La Service-Oriented Architecture est un style architectural apparu dans les années 2000 pour répondre à un problème massif dans les grandes entreprises : des dizaines de systèmes hétérogènes (ERP, CRM, legacy COBOL, partenaires B2B) incapables de se parler.

L’idée fondatrice : décomposer l’application en services métier indépendants, chacun exposant une interface publique formelle (son contrat), communicant via un bus central (l’ESB — Enterprise Service Bus) qui prend en charge le routage, la transformation de messages, la sécurité et le monitoring de manière transversale.

02

Vue d’ensemble

Une architecture SOA typique s’articule autour de trois zones : les consommateurs (clients qui appellent les services), l’ESB (le bus qui centralise la communication), et les services métier (les unités autonomes de logique).

Consommateurs
🌐 Web App
📱 Mobile App
🤝 Partenaire B2B
📦 ERP Interne
🖥️ Portail Client
ESB / Service Bus
⇄ Routage
⇌ Transformation
🎼 Orchestration
🔐 Sécurité / Auth
📊 Monitoring
📋 Logging
⚡ Cache
Services Métier (Business Services)
🛒 Svc Commande
💳 Svc Paiement
👤 Svc Client
📦 Svc Inventaire
🔔 Svc Notification
🔑 Svc Auth / IAM
📈 Svc Reporting
🚚 Svc Livraison
📒 Service Registry (UDDI / Annuaire)
Discover · Publish · Bind
Règle fondamentale : tout consommateur communique exclusivement via l’ESB. Il ne connaît jamais l’adresse réseau réelle d’un service — seulement son nom de contrat. L’ESB résout, route et transforme de manière transparente.
03

Le Triangle SOA — Publish / Find / Bind

Le modèle d’interaction canonique en SOA repose sur trois acteurs et trois opérations. C’est le protocole de découverte de services à la base de toute l’architecture.

Service Provider
Fournisseur
Service Registry
Annuaire
UDDI
Service Consumer
Consommateur
1. Publish2. Find3. Bind
Publish — Le fournisseur enregistre son service dans l’annuaire avec son contrat (WSDL ou OpenAPI).
Find — Le consommateur interroge l’annuaire pour trouver le service dont il a besoin.
Bind — Le consommateur appelle directement le service via le contrat découvert (souvent via l’ESB).
04

Les 8 Principes de Thomas Erl

Thomas Erl a formalisé huit principes fondamentaux qui définissent ce qu’est un vrai service SOA. Un service qui viole l’un de ces principes n’est pas un service — c’est une fonction exposée sur le réseau.

01

Loose Coupling

Les services minimisent leurs dépendances mutuelles. Un service ne connaît que le contrat de l’autre, jamais son implémentation.

02

Service Contract

Chaque service expose un contrat formel et stable — son interface publique. C’est le seul engagement envers l’extérieur.

03

Autonomy

Chaque service contrôle entièrement sa propre logique. Il ne délègue pas à un autre service pour remplir sa propre responsabilité.

04

Abstraction

L’implémentation interne est cachée. Les consommateurs ne voient que ce que le contrat expose — rien de plus.

05

Reusability

Un service est conçu pour être consommé par plusieurs clients. La réutilisation est une priorité de conception, pas un bonus.

06

Composability

Les services peuvent être composés pour former des processus métier plus complexes — l’orchestration en est l’expression.

07

Statelessness

Les services évitent de conserver un état entre les appels. L’état est porté par le client ou externalisé dans un store.

08

Discoverability

Les services sont enregistrés et découvrables via un annuaire. Aucun consommateur ne devrait coder en dur une adresse réseau.

05

Orchestration vs Chorégraphie

En SOA, deux modèles coexistent pour coordonner plusieurs services dans un processus métier. Le choix entre les deux est l’une des décisions d’architecture les plus importantes.

🎼 Orchestration — le Chef d’orchestre
Orchestrateur
(ESB / BPEL / Camunda)
1. Commande
2. Paiement
3. Livraison
Flux visible et facile à monitorer
Rollback / compensation centralisés
Point de défaillance unique (SPOF)
Couplage fort à l’orchestrateur
Outils : BPEL, Camunda, Activiti, Apache Camel
💃 Chorégraphie — la Danse collective
Event Bus (Kafka / RabbitMQ)
Svc Commande
émet: order.created
Svc Paiement
réagit: order.created
Svc Livraison
réagit: payment.ok
Svc Notif
réagit: any event
Pas de SPOF — très résilient
Services totalement découplés
Flux implicite, difficile à déboguer
Cohérence éventuelle complexe
Outils : Kafka, RabbitMQ, AWS EventBridge
Piège classique : l’orchestration centralisée dans l’ESB crée souvent un monolithe distribué. La logique métier migre progressivement dans l’ESB au lieu de rester dans les services — résultat : l’ESB devient le vrai système, et les services des façades vides.
06

Le Contrat de Service

En SOA, le contrat est roi. Avant d’écrire une ligne d’implémentation, on définit l’interface publique. C’est l’équivalent moderne du WSDL — et c’est le seul point de couplage autorisé entre services.

Consumer
App
dépend de
«interface»
IOrderService
implémente
OrderService
(impl.)
contracts/service.contracts.ts
// ── Types enveloppes — le "protocole" commun entre tous les services ── export interface ServiceRequest<T> { correlationId: string; // trace bout-en-bout timestamp: Date; payload: T; metadata?: Record<string, string>; } export interface ServiceResponse<T> { correlationId: string; success: boolean; data?: T; error?: ServiceError; timestamp: Date; } export interface ServiceError { code: string; message: string; details?: unknown; } // ── Contrat du Service Commande ── export interface IOrderService { createOrder(request: ServiceRequest<CreateOrderDTO>): Promise<ServiceResponse<OrderDTO>>; getOrder (request: ServiceRequest<{ orderId: string }>): Promise<ServiceResponse<OrderDTO>>; cancelOrder(request: ServiceRequest<{ orderId: string }>): Promise<ServiceResponse<void>>; } // ── Contrat du Service Paiement ── export interface IPaymentService { processPayment(request: ServiceRequest<PaymentDTO>): Promise<ServiceResponse<PaymentResultDTO>>; refund (request: ServiceRequest<RefundDTO>): Promise<ServiceResponse<void>>; } // ── DTOs (Data Transfer Objects) — ce qui traverse le réseau ── export interface CreateOrderDTO { customerId: string; items: Array<{ productId: string; quantity: number; unitPrice: number }>; } export interface OrderDTO { orderId: string; customerId: string; items: CreateOrderDTO['items']; totalAmount: number; status: 'PENDING' | 'CONFIRMED' | 'CANCELLED' | 'DELIVERED'; createdAt: Date; } export interface PaymentDTO { orderId: string; amount: number; currency: string; paymentMethod: 'CREDIT_CARD' | 'BANK_TRANSFER'; } export interface PaymentResultDTO { transactionId: string; status: 'SUCCESS' | 'FAILED' | 'PENDING'; } export interface RefundDTO { transactionId: string; reason: string; }
07

Implémentation — Approche Orientée Objet

Chaque service implémente son contrat de manière autonome. L’implémentation est un détail — le contrat est la vérité. NestJS facilite cette approche grâce à l’injection de dépendances et aux décorateurs.

order/order.service.ts
import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { v4 as uuidv4 } from 'uuid'; import { IOrderService, ServiceRequest, ServiceResponse, CreateOrderDTO, OrderDTO } from './contracts'; import { OrderEntity } from './entities/order.entity'; @Injectable() export class OrderService implements IOrderService { private readonly logger = new Logger(OrderService.name); constructor( @InjectRepository(OrderEntity) private readonly orderRepository: Repository<OrderEntity>, ) {} async createOrder( request: ServiceRequest<CreateOrderDTO>, ): Promise<ServiceResponse<OrderDTO>> { const { correlationId, payload } = request; this.logger.log(`[${correlationId}] Creating order for ${payload.customerId}`); try { const totalAmount = payload.items.reduce( (sum, item) => sum + item.quantity * item.unitPrice, 0, ); const order = this.orderRepository.create({ orderId: uuidv4(), customerId: payload.customerId, items: payload.items, totalAmount, status: 'PENDING', createdAt: new Date(), }); const saved = await this.orderRepository.save(order); return { correlationId, success: true, data: this.toDTO(saved), timestamp: new Date() }; } catch (error) { this.logger.error(`[${correlationId}] Failed to create order`, error); return { correlationId, success: false, error: { code: 'ORDER_CREATE_FAILED', message: 'Unable to create order', details: error }, timestamp: new Date(), }; } } async getOrder( request: ServiceRequest<{ orderId: string }>, ): Promise<ServiceResponse<OrderDTO>> { const { correlationId, payload } = request; const order = await this.orderRepository.findOne({ where: { orderId: payload.orderId } }); if (!order) { return { correlationId, success: false, error: { code: 'ORDER_NOT_FOUND', message: `Order ${payload.orderId} not found` }, timestamp: new Date(), }; } return { correlationId, success: true, data: this.toDTO(order), timestamp: new Date() }; } async cancelOrder( request: ServiceRequest<{ orderId: string }>, ): Promise<ServiceResponse<void>> { await this.orderRepository.update( { orderId: request.payload.orderId }, { status: 'CANCELLED' }, ); return { correlationId: request.correlationId, success: true, timestamp: new Date() }; } private toDTO(entity: OrderEntity): OrderDTO { return { orderId: entity.orderId, customerId: entity.customerId, items: entity.items, totalAmount: entity.totalAmount, status: entity.status, createdAt: entity.createdAt, }; } }
08

Le Service Bus — ESB simplifié

L’ESB est le cœur de la SOA. Pour comprendre ce qu’il fait, voici un ESB maison en TypeScript qui implémente les responsabilités essentielles : enregistrement, routage, middleware pipeline (logging, auth, retry).

C’est le pattern Chain of Responsibility de GoF appliqué au bus de services.

bus/service-bus.ts
// ── Interfaces du Bus ── interface BusContext { serviceName: string; method: string; request: ServiceRequest<unknown>; } interface Middleware { before(ctx: BusContext): Promise<void>; after (ctx: BusContext, res: ServiceResponse<unknown>): Promise<void>; } // ── Le Bus ── export class ServiceBus { private readonly services = new Map<string, unknown>(); private readonly middlewares: Middleware[] = []; register<T>(name: string, service: T): void { this.services.set(name, service); } use(middleware: Middleware): void { this.middlewares.push(middleware); } async call<TReq, TRes>( serviceName: string, method: string, payload: TReq, ): Promise<ServiceResponse<TRes>> { const correlationId = uuidv4(); const request: ServiceRequest<TReq> = { correlationId, timestamp: new Date(), payload }; const ctx: BusContext = { serviceName, method, request }; // ── Pipeline "avant" ── for (const mw of this.middlewares) await mw.before(ctx); try { const service = this.services.get(serviceName); if (!service) throw new Error(`Service "${serviceName}" not found`); const handler = (service as Record<string, Function>)[method]; if (typeof handler !== 'function') throw new Error(`Method "${method}" not found on "${serviceName}"`); const response: ServiceResponse<TRes> = await handler.call(service, request); // ── Pipeline "après" ── for (const mw of this.middlewares) await mw.after(ctx, response); return response; } catch (error) { return { correlationId, success: false, error: { code: 'BUS_ERROR', message: String(error) }, timestamp: new Date(), }; } } } // ── Middleware de logging ── class LoggingMiddleware implements Middleware { async before(ctx: BusContext): Promise<void> { console.log(`[BUS] --> ${ctx.serviceName}.${ctx.method} | ${ctx.request.correlationId}`); } async after(ctx: BusContext, res: ServiceResponse<unknown>): Promise<void> { const status = res.success ? 'OK' : 'FAIL'; console.log(`[BUS] <-- ${ctx.serviceName}.${ctx.method} | ${status}`); } } // ── Enregistrement et utilisation ── const bus = new ServiceBus(); bus.use(new LoggingMiddleware()); bus.register('OrderService', new OrderService(repo)); bus.register('PaymentService', new PaymentService()); // Le consommateur ne connaît PAS l'implémentation — seulement le bus const result = await bus.call<CreateOrderDTO, OrderDTO>( 'OrderService', 'createOrder', { customerId: 'cust-001', items: [{ productId: 'p-1', quantity: 2, unitPrice: 49.99 }] }, );
09

Orchestration — Pattern Saga avec Compensation

Dans un système distribué, les transactions ACID n’existent plus. Le pattern Saga remplace la transaction globale par une séquence d’étapes locales, chacune associée à une compensation en cas d’échec — l’équivalent d’un ROLLBACK distribué.

étape 1
Créer la commande
OrderService.createOrder()compensation: cancelOrder()
étape 2
Traiter le paiement
PaymentService.processPayment()compensation: refund()
étape 3
Notifier le client
NotificationService.sendConfirmation()pas de compensation nécessaire
succès
Publier l’événement domaine
EventEmitter.emit('order.confirmed')
orchestration/place-order.orchestrator.ts
export class PlaceOrderOrchestrator { constructor( private readonly bus: ServiceBus, private readonly eventEmitter: EventEmitter, ) {} async execute(customerId: string, items: CreateOrderDTO['items']): Promise<OrderDTO> { const steps: CompensationStep[] = []; // pile LIFO pour rollback // ── Étape 1 : Créer la commande ── const orderRes = await this.bus.call<CreateOrderDTO, OrderDTO>( 'OrderService', 'createOrder', { customerId, items }, ); if (!orderRes.success || !orderRes.data) throw new Error('Order creation failed'); const order = orderRes.data; steps.push({ compensate: () => this.bus.call('OrderService', 'cancelOrder', { orderId: order.orderId }), }); // ── Étape 2 : Traiter le paiement ── const payRes = await this.bus.call<PaymentDTO, PaymentResultDTO>( 'PaymentService', 'processPayment', { orderId: order.orderId, amount: order.totalAmount, currency: 'EUR', paymentMethod: 'CREDIT_CARD' }, ); if (!payRes.success || payRes.data?.status !== 'SUCCESS') { await this.compensate(steps); // rollback de tout throw new Error('Payment failed — order rolled back'); } steps.push({ compensate: () => this.bus.call('PaymentService', 'refund', { transactionId: payRes.data!.transactionId, reason: 'Orchestration rollback', }), }); // ── Étape 3 : Notification ── await this.bus.call('NotificationService', 'sendOrderConfirmation', { customerId, orderId: order.orderId, transactionId: payRes.data!.transactionId, }); // ── Événement domaine ── this.eventEmitter.emit('order.confirmed', { orderId: order.orderId, customerId }); return order; } private async compensate(steps: CompensationStep[]): Promise<void> { // LIFO — on défait dans l'ordre inverse for (const step of [...steps].reverse()) { try { await step.compensate(); } catch (e) { console.error('Compensation failed', e); } } } } interface CompensationStep { compensate: () => Promise<unknown>; }
10

Approche Fonctionnelle — Pipeline fp-ts

La SOA n’est pas réservée à la POO. En programmation fonctionnelle, un service est simplement une fonction pureInput → TaskEither<Error, Output>. La composition de services devient alors un pipeline explicite via pipe et chain.

La bibliothèque fp-ts apporte les monades TaskEither et Either qui encodent les deux cas — succès et échec — dans le type lui-même. Plus de try/catch implicites.

functional/order-pipeline.ts
import { pipe } from 'fp-ts/function'; import * as TE from 'fp-ts/TaskEither'; import { v4 as uuidv4 } from 'uuid'; // ── Type canonique d'un service fonctionnel ── type ServiceCall<A, B> = (input: A) => TE.TaskEither<ServiceError, B>; // ── Services en style fonctionnel (fonctions pures) ── const createOrder: ServiceCall<CreateOrderDTO, OrderDTO> = (payload) => TE.tryCatch( async () => ({ orderId: uuidv4(), customerId: payload.customerId, items: payload.items, totalAmount: payload.items.reduce((s, i) => s + i.quantity * i.unitPrice, 0), status: 'PENDING' as const, createdAt: new Date(), }), (e): ServiceError => ({ code: 'ORDER_FAILED', message: String(e) }), ); const processPayment: ServiceCall<PaymentDTO, PaymentResultDTO> = (payload) => TE.tryCatch( async () => ({ transactionId: uuidv4(), status: 'SUCCESS' as const, }), (e): ServiceError => ({ code: 'PAYMENT_FAILED', message: String(e) }), ); // ── Composition du pipeline ── const placeOrderPipeline = ( customerId: string, items: CreateOrderDTO['items'], ): TE.TaskEither<ServiceError, PaymentResultDTO> => pipe( createOrder({ customerId, items }), // TaskEither<Err, OrderDTO> TE.chain((order) => // si succès, on enchaîne processPayment({ orderId: order.orderId, amount: order.totalAmount, currency: 'EUR', paymentMethod: 'CREDIT_CARD', }), ), TE.tapError((err) => // log en cas d'erreur TE.of(console.error('Pipeline failed:', err.code)) ), ); // ── Appel ── const result = await placeOrderPipeline( 'cust-001', [{ productId: 'p-1', quantity: 2, unitPrice: 49.99 }], )(); if (result._tag === 'Right') { console.log('Success:', result.right.transactionId); } else { console.error('Failed:', result.left.code); }
Avantage clé : dans l’approche fonctionnelle, l’erreur est un citoyen de première classe du type. Le compilateur TypeScript te force à gérer les deux cas. Il est impossible d’oublier un cas d’échec — contrairement aux try/catch où l’oubli passe silencieusement à la compilation.
11

SOA vs Microservices

La confusion entre SOA et Microservices est omniprésente. La vérité : les Microservices sont une évolution de la SOA, nés du besoin de déploiements indépendants et de l’essor du Cloud. Ils partagent les mêmes principes fondamentaux, mais s’opposent radicalement sur l’implémentation.

CritèreSOA (2000–2010)Microservices (2010–aujourd’hui)
GranularitéServices métier larges (Order, Customer…)Services ultra-granulaires (1 fonction = 1 service)
CommunicationESB central — SOAP/XML, WS-*Direct — REST, gRPC, Events (Kafka)
DéploiementSouvent déployé en groupe (plusieurs services/serveur)Conteneur indépendant (Docker + K8s)
DonnéesBase de données souvent partagéeDatabase-per-service (idéal)
GouvernanceCentralisée — IT/Enterprise drivenEquipe autonome — Team owned
ContratWSDL / UDDI formelsOpenAPI / AsyncAPI
CibleIntégration Enterprise (EAI)Applications Cloud-native
Failure modeSPOF potentiel sur l’ESBDéfaillance partielle, circuit breaker
🏢

La métaphore finale

La SOA est une grande entreprise structurée avec des départements bien définis et un secrétariat central (l’ESB) qui filtre toute communication. Les Microservices sont une startup agile où chaque petite équipe autonome livre directement, sans passer par la hiérarchie. Les deux sont valides — ça dépend de ton contexte, de ta maturité organisationnelle et de tes contraintes de déploiement.

12

Quand utiliser la SOA ?

✓ Utilise la SOA quand…
Tu dois intégrer des systèmes hétérogènes existants (ERP, CRM, legacy, partenaires B2B)
Tu as besoin d’une gouvernance forte et de contrats formels (finance, santé, gouvernement)
La réutilisation de services entre plusieurs applications est un objectif stratégique
Tu travailles dans un contexte Enterprise avec des équipes IT centralisées
Les protocoles de sécurité standardisés (WS-Security, SAML) sont requis
✗ Évite la SOA quand…
Tu construis une startup — l’overhead d’un ESB est fatal à la vélocité
Tu veux un déploiement indépendant par feature team → préfère les Microservices
Ton système est simple — un bon Monolithe Modulaire est plus efficace et moins coûteux
Tu as besoin d’une latence ultra-faible — l’ESB ajoute des sauts réseau
Ton équipe n’a pas la maturité opérationnelle pour maintenir un ESB en production
Règle de Rob Martin appliquée à la SOA : chaque service doit avoir une seule raison de changer — son domaine métier. Si un service change parce que l’ESB change, ou parce que la base de données change, la SOA a échoué. Le Principe de Responsabilité Unique s’applique à l’échelle de l’architecture, pas seulement du code.
Consommateurs
ESB / Bus
Services Métier
Annuaire / Registry
Compensation / Saga
Contrat / Interface