// 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.
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.
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é
Nginx
Kong
AWS GW
NestJS
Reverse proxy / routing
✓
✓
✓
✓
TLS / HTTPS termination
✓
✓
✓
✓
Rate limiting
basique
✓
✓
✓
Auth JWT / OAuth
✗
plugin
✓
✓ Guard
Logique métier custom
✗
limité
limité
✓ full TS
Maintenance / ops
simple
modérée
modérée
tu 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 IPlimit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
server {
listen80;
# /orders/** → service Commandeslocation /orders/ {
limit_reqzone=api burst=20;
proxy_passhttp://orders-service:3001; ← reverse proxyproxy_set_header X-Real-IP $remote_addr;
}
# /users/** → service Userslocation /users/ {
proxy_passhttp://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.
@Injectable()
export classAuthGuardimplementsCanActivate {
constructor(private jwt: JwtService) {}
canActivate(ctx: ExecutionContext): boolean {
const req = ctx.switchToHttp().getRequest();
const token = req.headers.authorization?.split(' ')[1];
if (!token) throw newUnauthorizedException();
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 newUnauthorizedException('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.
fig. 3 — anatomie d'un service métier
08
Implémentation complète
order.entity.ts
@Entity('orders')
export classOrder {
@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 classOrdersService {
constructor(
@InjectRepository(Order) private readonly repo: Repository<Order>,
private readonly events: OrdersEventsService,
) {}
asynccreate(dto: CreateOrderDto, userId: string): Promise<Order> {
const total = dto.items.reduce((sum, i) => sum + i.price * i.qty, 0);
const order = awaitthis.repo.save(this.repo.create({ ...dto, userId, total }));
// fire-and-forget — on ne sait pas qui va consommer cet eventawaitthis.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:5672DB_HOST: orders-dbpayments-service:
build: ./payments-service
# ← pas de ports: — aucune exposition HTTP# écoute uniquement RabbitMQ
environment:
RABBITMQ_URL: amqp://rabbitmq:5672orders-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.
fig. 4 — appels directs vs message broker
RabbitMQ vs Kafka
Critère
RabbitMQ
Kafka
Modèle
Queue — message supprimé après lecture
Log — message conservé, offset par consommateur
Volume
Des milliers de msg/s
Des millions de msg/s
Replay
Non
Oui — rejouer l’historique
Complexité ops
Simple
Élevée (ZooKeeper/KRaft)
Cas d'usage
Projets standards, microservices classiques
Analytics, event sourcing, très haut volume
10
RabbitMQ + NestJS
orders-events.service.ts — producteur
@Injectable()
export classOrdersEventsService {
constructor(
@Inject('BROKER') private readonly client: ClientProxy,
) {}
asyncemit(pattern: string, payload: unknown): Promise<void> {
// fire-and-forget : on n'attend pas de réponsethis.client.emit(pattern, payload);
}
}
payments.controller.ts — consommateur
@Controller()
export classPaymentsController {
constructor(private readonly svc: PaymentsService) {}
// écoute l'event — sans rien changer dans orders-service@EventPattern('order.created')
asynchandleOrderCreated(
@Payload() data: { orderId: string; userId: string; total: number },
) {
awaitthis.svc.processPayment(data);
}
}
// notifications.controller.ts — écoute le MÊME event en parallèle@EventPattern('order.created')
asynchandleOrderCreated(@Payload() data) {
awaitthis.svc.sendConfirmationEmail(data);
}
main.ts — service consommateur (pas de port HTTP)
async functionbootstrap() {
// createMicroservice au lieu de create → écoute le broker, pas HTTPconst app = awaitNestFactory.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.
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.