// Architecture Distribuée — Sam Newman · Martin Fowler

Architecture
Microservices

Du monolithe au système distribué — schémas, métaphores et code NestJS concret pour comprendre chaque composant : Gateway, services métier, Message Broker.

NestJSTypeScriptRabbitMQDockerNginxDistributed Systems
01

Monolithe vs Microservices

L'architecture microservices consiste à découper une application en services indépendants qui communiquent entre eux. Pour comprendre pourquoi, il faut partir de son opposé : le monolithe.

MONOLITHEUne seule application déployéeAuth+ Users + PaiementCatalogue+ Commandes + StockNotifications+ Emails + Push1 seule base de donnéesMICROSERVICESAuthserviceUsersserviceCatalogueserviceCommandesservicePaiementserviceNotifsserviceDBDBDBDBChaque service a sa propre DB
fig. 1 — monolithe vs architecture microservices
🧱
Monolithe
Tout dans une seule app
Déploiement unique. Si la cuisine prend feu, tout l'hôtel ferme. Agrandir une salle nécessite de rénover tout le bâtiment.
🏙️
Microservices
Services indépendants
Chaque commerce est autonome, a sa propre adresse, sa propre équipe, peut ouvrir/fermer sans affecter les voisins.
02

La Métaphore fondatrice

🏬

Le Centre Commercial

Imagine un grand centre commercial. Chaque boutique est autonome : elle gère ses propres stocks, ses propres employés, ses propres horaires. La librairie peut faire des travaux sans fermer la boulangerie. Si la boutique de téléphones connaît un rush, on ajoute des vendeurs sans toucher aux autres.

Le hall d'entrée est l'API Gateway : tout le monde passe par là, on vérifie les sacs (auth), on indique où aller (routing). Les boutiques communiquent parfois par interphone (HTTP sync) ou déposent des messages en caisse centrale (broker async) que les autres récupèrent quand elles sont disponibles.

Cette métaphore capture les trois principes fondamentaux des microservices : l'autonomie de déploiement (chaque service se déploie indépendamment), l'isolation des données (chaque boutique gère son propre stock), et la communication découplée (les boutiques ne partagent pas de caisse commune).

03

Vue d'ensemble — Architecture complète

Une architecture microservices en production se compose de plusieurs couches distinctes. Chaque couche a une responsabilité précise.

CLIENTSWeb AppReact / VueMobileiOS / AndroidAPI tiersPartenairesAPI GATEWAYAPI GatewayAuth · Rate limiting · Routing · Load balancingSERVICES MÉTIERAuthJWT · sessionsUsersProfils · rôlesCommandesCRUD · statutsPaiementStripe · facturationNotifsEmail · push · SMSCOMMUNICATION ASYNCMessage BrokerKafka · RabbitMQ — événements asynchronesPERSISTENCE — 1 DB PAR SERVICEPostgresusers / authPostgresusers dataMongoDBcommandesPostgrestransactionsRedisqueues / cacheINFRASTRUCTURE TRANSVERSEService DiscoveryConsul · KubernetesObservabilitéLogs · traces · métriquesCircuit BreakerResilience4j · HystrixOrchestrationDocker · Kubernetes · CI/CDServices métierGatewayMessage brokerInfrastructureBases de donnéesChaque service est déployé indépendamment dans son propre container Docker
fig. 2 — architecture complète en production
04

API Gateway — Le point d'entrée unique

L'API Gateway est le réceptionniste de l'architecture. Personne n'entre dans le système sans passer par lui. Il centralise toutes les préoccupations transverses.

Responsabilités du Gateway — Vérification JWT · routing vers les services · rate limiting · load balancing · enrichissement des headers (injection de x-user-id)
⚙️
Option A
Outil dédié — Kong / Nginx
Configuration déclarative YAML. Zéro code métier. Idéal quand on veut moins de choses à maintenir.
🔧
Option B
Application NestJS custom
Code TypeScript complet. Guards, Interceptors, Pipes. Idéal pour la logique métier fine au niveau du routing.

Spectre des responsabilités

FonctionnalitéNginxKongAWS GWNestJS
Reverse proxy / routing
TLS / HTTPS termination
Rate limitingbasique
Auth JWT / OAuthplugin✓ Guard
Logique métier customlimitélimité✓ full TS
Maintenance / opssimplemodéréemodéréetu gères tout
En pratique — Nginx et NestJS ne s'excluent pas — ils se combinent. Nginx gère TLS + compression en frontal, NestJS gère la logique JWT + routing derrière.
05

Pourquoi Nginx peut remplacer NestJS

Un API Gateway et un reverse proxy font la même chose fondamentalement : rediriger du trafic HTTP. La différence est une question de degré, pas de nature.

La directive proxy_pass de Nginx fait exactement ce que fait this.http.request() dans un ProxyService NestJS — les deux opèrent un reverse proxy HTTP.

nginx.conf
# Nginx comme API Gateway basique http { # Rate limiting : max 10 req/s par IP limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; server { listen 80; # /orders/** → service Commandes location /orders/ { limit_req zone=api burst=20; proxy_pass http://orders-service:3001; ← reverse proxy proxy_set_header X-Real-IP $remote_addr; } # /users/** → service Users location /users/ { proxy_pass http://users-service:3002; } } }
Limite de Nginx seul — Nginx ne peut pas lire ni valider un JWT — ce n'est pas un interpréteur de code. Pour l'auth, il faut Kong (plugin JWT), AWS API Gateway, ou du code (NestJS).
06

Gateway NestJS complet

Un API Gateway NestJS est une application Nest standard dont les controllers ne font aucune logique métier — ils proxifient uniquement. L'auth est portée par un Guard.

gateway.controller.ts
@Controller() export class GatewayController { constructor(private readonly proxy: ProxyService) {} @All('orders/*') @UseGuards(AuthGuard) // vérifie JWT — seul endroit où c'est fait @UseInterceptors(LoggingInterceptor) async proxyOrders( @Req() req: Request, @Res() res: Response, @User() user: JwtPayload, ) { return this.proxy.forward(req, res, { target: 'http://orders-service:3001', headers: { 'x-user-id': user.id }, // enrichit la requête }); } @Post('auth/login') // route publique — pas de Guard async login(@Req() req, @Res() res) { return this.proxy.forward(req, res, { target: 'http://auth-service:3003', }); } }
auth.guard.ts
@Injectable() export class AuthGuard implements CanActivate { constructor(private jwt: JwtService) {} canActivate(ctx: ExecutionContext): boolean { const req = ctx.switchToHttp().getRequest(); const token = req.headers.authorization?.split(' ')[1]; if (!token) throw new UnauthorizedException(); try { // Gateway = SEUL point de vérification JWT // Les services downstream font confiance à x-user-id req.user = this.jwt.verify(token); return true; } catch { throw new UnauthorizedException('Invalid or expired token'); } } }
07

Structure d'un service métier

Un service métier est une application NestJS autonome avec une responsabilité unique. Sa caractéristique principale : ce qu'il ne fait pas.

🚫
Ne fait pas
Pas d'auth, pas de JWT
Il fait confiance au header x-user-id injecté par le Gateway. L'auth est déjà vérifiée en amont.
🚫
Ne fait pas
Pas d'import d'autres services
Jamais import CatalogueService. La communication passe par HTTP ou par events — jamais par import direct.
Fait
Sa propre base de données
Chaque service possède sa DB isolée. Le service Auth peut changer son schéma sans casser les autres.
Fait
Émet des events
Après chaque action importante, il publie un event sur le broker. Il ne sait pas qui l'écoutera.
API Gatewayx-user-id headerMessage Brokerevents entrantsORDERS-SERVICEApplication NestJS autonome · port 3001ControllerHTTP routes · validation DTOsServiceLogique métier · orchestrationRepositoryTypeORM · accès DBEventEmitter → BrokerHTTP Responsevers GatewayPostgresDB dédiéeorders onlyEvents sortantsorder.created...Le service ne sait pas qui l'appelle ni qui consomme ses events
fig. 3 — anatomie d'un service métier
08

Implémentation complète

order.entity.ts
@Entity('orders') export class Order { @PrimaryGeneratedColumn('uuid') id: string; @Column() userId: string; // pas de FK vers User — juste l'ID @Column('jsonb') items: OrderItem[]; @Column({ type: 'enum', enum: OrderStatus, default: OrderStatus.PENDING }) status: OrderStatus; @Column('decimal') total: number; }
orders.service.ts
@Injectable() export class OrdersService { constructor( @InjectRepository(Order) private readonly repo: Repository<Order>, private readonly events: OrdersEventsService, ) {} async create(dto: CreateOrderDto, userId: string): Promise<Order> { const total = dto.items.reduce((sum, i) => sum + i.price * i.qty, 0); const order = await this.repo.save(this.repo.create({ ...dto, userId, total })); // fire-and-forget — on ne sait pas qui va consommer cet event await this.events.emit('order.created', { orderId: order.id, userId, total, items: dto.items, }); return order; // répond AVANT que Paiement ou Notifs aient traité } }
docker-compose.yml — extrait
services: orders-service: build: ./orders-service ports: - "3001:3001" # expose HTTP — reçoit du Gateway environment: RABBITMQ_URL: amqp://rabbitmq:5672 DB_HOST: orders-db payments-service: build: ./payments-service # ← pas de ports: — aucune exposition HTTP # écoute uniquement RabbitMQ environment: RABBITMQ_URL: amqp://rabbitmq:5672 orders-db: image: postgres:16 # DB privée — aucun autre service n'y a accès
09

La boîte aux lettres du système

Le broker casse le modèle de communication synchrone auquel on est habitué. La métaphore la plus juste : la lettre de courrier.

Quand tu envoies une lettre, tu ne restes pas debout devant ta boîte à attendre la réponse. Tu déposes et tu passes à autre chose. Le destinataire lira quand il sera disponible. Si tu envoies une circulaire, plusieurs personnes lisent la même lettre.

SANS BROKER — couplage fortCommandesserviceHTTPPaiementNotifsStockSi Paiement est down → Commandes échoue.Commandes doit connaître chaque service.Ajout d'un service → modifier Commandes.AVEC BROKER — découplage fortCommandesémet et oublieBrokerorder.createdfile d'attentePaiementNotifsStockSi Paiement est down → le message l'attend.Commandes ne connaît aucun consommateur.Ajouter Analytics → brancher sans modifier Commandes.Commandes répond au client AVANT que Paiement et Notifs aient traité quoi que ce soit
fig. 4 — appels directs vs message broker

RabbitMQ vs Kafka

CritèreRabbitMQKafka
ModèleQueue — message supprimé après lectureLog — message conservé, offset par consommateur
VolumeDes milliers de msg/sDes millions de msg/s
ReplayNonOui — rejouer l’historique
Complexité opsSimpleÉlevée (ZooKeeper/KRaft)
Cas d'usageProjets standards, microservices classiquesAnalytics, event sourcing, très haut volume
10

RabbitMQ + NestJS

orders-events.service.ts — producteur
@Injectable() export class OrdersEventsService { constructor( @Inject('BROKER') private readonly client: ClientProxy, ) {} async emit(pattern: string, payload: unknown): Promise<void> { // fire-and-forget : on n'attend pas de réponse this.client.emit(pattern, payload); } }
payments.controller.ts — consommateur
@Controller() export class PaymentsController { constructor(private readonly svc: PaymentsService) {} // écoute l'event — sans rien changer dans orders-service @EventPattern('order.created') async handleOrderCreated( @Payload() data: { orderId: string; userId: string; total: number }, ) { await this.svc.processPayment(data); } } // notifications.controller.ts — écoute le MÊME event en parallèle @EventPattern('order.created') async handleOrderCreated(@Payload() data) { await this.svc.sendConfirmationEmail(data); }
main.ts — service consommateur (pas de port HTTP)
async function bootstrap() { // createMicroservice au lieu de create → écoute le broker, pas HTTP const app = await NestFactory.createMicroservice<MicroserviceOptions>( AppModule, { transport: Transport.RMQ, options: { urls: [process.env.RABBITMQ_URL], queue: 'events_queue', }, }, ); await app.listen(); // tourne en permanence, aucun port exposé }
11

Flux bout en bout — Passer une commande

Voici comment une requête POST /orders traverse l'intégralité de l'architecture.

ClientGatewayAuthCommandesPaiementNotifs1POST /orders2validate JWT✓ user_id3forward + x-user-id4DBsave order5event: order.created (async)← Les étapes suivantes sont parallèles et indépendantes →6Stripeprocess payment7SMTPsend email8201 Created { order_id } — réponse sync immédiatesync HTTPasync eventréponse
fig. 5 — séquence complète d'une commande
Point clé — Cohérence éventuelle — Le service Commandes répond 201 Created immédiatement au client (étape 8), sans attendre que le paiement soit prélevé ni que l'email soit envoyé. Le système converge vers un état cohérent, mais pas instantanément.
12

Quand adopter les microservices ?

Les microservices ne sont pas la solution par défaut. Le monolithe reste souvent plus sage au départ.

1
Projet solo ou petite équipe (<5 personnes) — reste sur le monolithe. La complexité opérationnelle des microservices est disproportionnée.
2
Équipes qui grossissent — le monolithe devient un goulot organisationnel autant que technique. Les équipes se bloquent mutuellement au déploiement.
3
Besoin de scalabilité différenciée — Netflix scale son service streaming ×100 sans toucher à la facturation. Impossible en monolithe.
4
Déploiement continu à grande échelle — Amazon déploie en production des milliers de fois par jour. Chaque équipe pousse son service indépendamment.
13

Pièges classiques — Ce qui peut mal tourner

💥
Piège 01
Explosion de la complexité ops
Tu échanges les bugs de code contre des bugs réseau et de configuration distribuée. Chaque service = un processus, une DB, une config à monitorer.
🔄
Piège 02
Transactions distribuées
Sans transaction ACID multi-services, tu dois implémenter des patterns comme SAGA ou 2PC. C'est complexe et source d'incohérences.
⏱️
Piège 03
Latence en cascade
5 appels HTTP synchrones en chaîne = 5× les délais réseau. Un service lent dégrade toute la chaîne sans circuit breaker.
💀
Piège 04
Death star diagram
Quand chaque service appelle tous les autres sans discipline, tu recréés un monolithe distribué — le pire des deux mondes.
Règle d'or — Un service ne fait jamais import d'un autre service. La communication passe uniquement par HTTP ou par events via le broker. C'est ce qui garantit l'isolation.