8.4 — Architectures backend avancées
Avancé
🎯 Objectif : connaître les patterns d’architecture qu’on attend d’un dev senior — monolith vs microservices, DDD, hexagonal, event-driven — pour savoir quand utiliser quoi sans tomber dans le sur-engineering.
À l'issue de cet axe, tu sauras :
- Choisir entre monolith, modulith, microservices et serverless selon le contexte (et savoir argumenter)
- Appliquer Domain-Driven Design en mode léger : entités, agrégats, value objects, bounded contexts
- Structurer un service avec hexagonal / ports & adapters
- Comprendre clean architecture (use cases, inversion de dépendances)
- Concevoir un système event-driven : event bus, queues, CQRS, outbox pattern
- Reconnaître quand un sujet est de l'over-engineering pour ton contexte
💡 Termes glossaire : DDD , CQRS , event sourcing , saga , bounded context .
1. Monolith vs modulith vs microservices vs serverless
Section intitulée « 1. Monolith vs modulith vs microservices vs serverless »Le faux débat
Section intitulée « Le faux débat »Trop souvent on oppose « monolith » à « microservices » comme si c’était binaire. La réalité est un spectre :
[ Monolith ] ─→ [ Modulith ] ─→ [ Microservices ] ─→ [ Serverless ] 1 process 1 process N processes N fonctions 1 codebase N modules N codebases N déploiements 1 DB 1 DB N DBs (idéal) DBs partagés ou par fnQuand utiliser quoi — décideur en 4 questions
Section intitulée « Quand utiliser quoi — décideur en 4 questions »| Question | Si oui → | Si non → |
|---|---|---|
| Tu es < 10 devs ? | Monolith ou modulith | Microservices peuvent commencer à se justifier |
| Tu as des équipes qui scalent indépendamment ? | Microservices | Monolith |
| Tu as du trafic burst (spike → 0) ? | Serverless | Monolith ou microservices avec instances réservées |
| Tu maîtrises Kubernetes / observabilité distribuée ? | Microservices OK | Reste en monolith — tu vas souffrir |
Le modulith : le pattern oublié
Section intitulée « Le modulith : le pattern oublié »Le monolith modulaire (modulith) est le bon défaut 2026 pour 80 % des cas :
- 1 process, 1 codebase, 1 DB (= simplicité opérationnelle d’un monolith)
- N modules avec des frontières strictes (= pré-découpe pour microservices futurs)
- Communication inter-modules via events internes (pas appels directs)
// Exemple modulith — chaque dossier = un module avec frontière strictesrc/├── modules/│ ├── auth/ ← peut être extrait en microservice plus tard│ │ ├── domain/│ │ ├── application/│ │ └── infrastructure/│ ├── billing/│ └── tasks/└── shared/ └── events/ ← bus interne, pas HTTPPiège réel rencontré — Le piège « microservices dès le jour 1 » archi
Symptôme : ton équipe de 4 devs déploie 12 microservices Kubernetes avec service mesh, observability stack, et 8 mois plus tard ne livre toujours pas de feature.
Cause : on confond « ça scale » (tech) avec « ça scale humainement » (équipe). À 4 devs, le coût opérationnel des microservices > bénéfice technique. Tu paies le prix sans encaisser la valeur.
Fix : commencer en modulith avec des modules clairs. Quand 2 modules sont touchés par 2 équipes différentes >50 % du temps, extraire ce module en microservice. Pas avant.
2. Domain-Driven Design light
Section intitulée « 2. Domain-Driven Design light »DDD complet (Eric Evans) = livre de 500 pages. La version utile au quotidien tient en 5 concepts.
Les 5 briques DDD à connaître
Section intitulée « Les 5 briques DDD à connaître »2.1 Entité
Section intitulée « 2.1 Entité »Objet qui a une identité (un id) qui le distingue, même si ses propriétés changent.
class User { constructor(public readonly id: UserId, public email: string) {} // Deux User avec même id = même user, même si email diffère}2.2 Value Object
Section intitulée « 2.2 Value Object »Objet identifié par ses valeurs. Immuable. Pas d’id.
class Money { constructor(public readonly amount: number, public readonly currency: 'EUR' | 'USD') {} add(other: Money): Money { if (this.currency !== other.currency) throw new Error('Currency mismatch'); return new Money(this.amount + other.amount, this.currency); }}// Deux Money(100, 'EUR') sont strictement égauxPourquoi c’est utile : tu rends impossibles les bugs comme « ajouter 100 EUR à 100 USD ». Le compilateur (ou le constructeur) refuse.
2.3 Agrégat
Section intitulée « 2.3 Agrégat »Un groupe d’entités qui forme une unité de cohérence. Une seule racine d’agrégat est exposée à l’extérieur ; les autres entités du groupe sont privées.
class Order { // Order = racine d'agrégat private items: OrderItem[] = []; // OrderItem = entité interne, pas exposée
addItem(product: Product, quantity: number) { if (this.status !== 'draft') throw new Error('Order is locked'); this.items.push(new OrderItem(product.id, quantity)); }}// Tu ne peux PAS faire `order.items[0].quantity = 99` depuis l'extérieur// Tu DOIS passer par `order.changeItemQuantity(...)` qui peut validerRègle d’or : 1 agrégat = 1 transaction DB. Si tu modifies 2 agrégats dans 1 transaction, tu mélanges des concepts qui devraient être séparés.
2.4 Bounded Context
Section intitulée « 2.4 Bounded Context »Un périmètre dans lequel un mot a un sens précis. Le même mot peut avoir des sens différents dans 2 contextes.
Context « Sales » Context « Shipping »- User = client (CRM) - User = destinataire (adresse)- Order = commande payée - Order = colis à expédier- Product = SKU + prix - Product = poids + dimensionsBénéfice : tu n’es pas forcé d’avoir un seul modèle User ou Order qui couvre tous les cas. Chaque bounded context a son propre modèle, optimisé pour son besoin.
C’est le pattern qui justifie les microservices : un microservice = un bounded context.
2.5 Repository
Section intitulée « 2.5 Repository »Interface qui abstrait la persistance. Le domaine ne sait pas si c’est SQL, NoSQL, ou en mémoire.
// Domaine — pure, pas d'import DBinterface UserRepository { findById(id: UserId): Promise<User | null>; save(user: User): Promise<void>;}
// Infrastructure — implémentation concrèteclass PostgresUserRepository implements UserRepository { async findById(id: UserId) { /* SELECT … */ } async save(user: User) { /* INSERT/UPDATE … */ }}C’est ce qui rend l’inversion de dépendances possible (cf. § 4 ci-dessous).
3. Hexagonal architecture (ports & adapters)
Section intitulée « 3. Hexagonal architecture (ports & adapters) »Le principe
Section intitulée « Le principe »Le domaine au centre. Tout ce qui touche au monde extérieur (HTTP, DB, queue, mail) est branché via des ports (interfaces) que des adapters implémentent.
┌──────────────────┐ │ HTTP Handler │ ← adapter "primary" (entrée) └────────┬─────────┘ │ port (interface) ▼ ┌──────────────────┐ │ Domain │ ← cœur, pure logique métier │ + Use Cases │ └────────┬─────────┘ │ port (interface) ▼ ┌──────────┬──────────────┬─────────────┐ ▼ ▼ ▼ ▼ Postgres Stripe Resend S3 (adapter) (adapter) (adapter) (adapter)Avantages concrets
Section intitulée « Avantages concrets »| Bénéfice | Comment c’est possible |
|---|---|
| Tests unitaires sans mocking lourd | Tu remplaces les adapters DB/Stripe par des fakes en mémoire pour tester le domaine |
| Swap d’infra | Tu peux passer de Postgres à MongoDB en réécrivant juste l’adapter, sans toucher au domaine |
| Domaine indépendant du framework | Tu peux changer Express → Hono sans toucher à la logique métier |
Structure de dossiers concrète
Section intitulée « Structure de dossiers concrète »src/├── domain/ ← cœur, 0 import HTTP / DB / framework│ ├── entities/│ ├── value-objects/│ └── ports/ ← interfaces abstraites (UserRepository, EmailSender)│├── application/ ← orchestrateur, use cases│ └── use-cases/ ← CreateUser, SubmitOrder, …│├── infrastructure/ ← implémentations concrètes des ports│ ├── persistence/ ← PostgresUserRepository│ ├── messaging/ ← StripeAdapter, ResendAdapter│ └── http/ ← Express/Hono routes (adapters primary)│└── presentation/ ← UI ou API JSON (Hono routes, GraphQL resolvers, …)Piège réel rencontré — L'hexagonal sur un projet de 500 lignes = sur-engineering archi
Symptôme : tu lances un side-project, tu prends 3 jours pour structurer en hexagonal avec ports / adapters / use cases avant d’écrire la 1re feature. Au bout de 2 semaines tu as 30 fichiers et 0 utilisateur.
Cause : hexagonal a un coût initial fixe (ratio code structure / code métier) qui est rentable au-delà d’un certain seuil. En dessous, c’est de la dette.
Fix : commence en monolith plat (src/routes/, src/db/). Quand tu as 3+ adapters différents pour le même service (ex. tu envoies des emails via Resend en prod, MailHog en dev, Mailtrap en preview), c’est le signal pour passer en hexagonal. Pas avant.
4. Clean Architecture (Uncle Bob)
Section intitulée « 4. Clean Architecture (Uncle Bob) »Variante très proche de l’hexagonal, avec 4 couches concentriques et la règle :
Les dépendances pointent vers l’intérieur. Une couche externe peut connaître l’interne, jamais l’inverse.
┌─────────────────────────────────────────────┐│ Frameworks & Drivers (Express, Postgres) │ ← niveau 4 (extérieur)│ ┌───────────────────────────────────────┐ ││ │ Interface Adapters (controllers) │ │ ← niveau 3│ │ ┌─────────────────────────────────┐ │ ││ │ │ Use Cases (business logic) │ │ │ ← niveau 2│ │ │ ┌───────────────────────────┐ │ │ ││ │ │ │ Entities (domain core) │ │ │ │ ← niveau 1 (cœur)│ │ │ └───────────────────────────┘ │ │ ││ │ └─────────────────────────────────┘ │ ││ └───────────────────────────────────────┘ │└─────────────────────────────────────────────┘En pratique avec TypeScript
Section intitulée « En pratique avec TypeScript »// Niveau 1 — Entity (domaine pur)class User { constructor(public readonly id: string, public email: string) {}}
// Niveau 2 — Use Caseclass RegisterUser { constructor( private users: UserRepository, // port private hash: PasswordHasher, // port private events: EventBus // port ) {} async execute(email: string, password: string) { if (await this.users.findByEmail(email)) throw new EmailTaken(); const user = new User(generateId(), email); await this.users.save(user, await this.hash.hash(password)); await this.events.publish(new UserRegistered(user.id)); return user; }}
// Niveau 3 — Controller (adapter primary)class UserController { constructor(private registerUser: RegisterUser) {} async post(req: Request) { const user = await this.registerUser.execute(req.body.email, req.body.password); return Response.json(user, { status: 201 }); }}
// Niveau 4 — Framework (Express route)app.post('/users', (req, res) => userController.post(req, res));Différence avec hexagonal : Clean Architecture ajoute une couche « Use Cases » entre l’entité et l’adapter. Hexagonal met les use cases avec le domaine. En pratique, les deux sont équivalents pour 95 % des projets.
5. Event-driven architecture
Section intitulée « 5. Event-driven architecture »Le pivot mental
Section intitulée « Le pivot mental »Au lieu de « A appelle B qui appelle C » (couplage fort), tu fais « A publie un événement, B et C l’écoutent » (couplage faible).
// Couplé (synchronous)async function placeOrder(order) { await chargeCard(order); await sendEmail(order); await updateStock(order); await notifyShipping(order);}// Si sendEmail() est lent, tout ralentit. Si updateStock() throw, l'order n'est pas créé.
// Event-driven (asynchronous)async function placeOrder(order) { await db.save(order); await eventBus.publish(new OrderPlaced(order)); // sendEmail, updateStock, notifyShipping écoutent OrderPlaced indépendamment}Outils de message broker
Section intitulée « Outils de message broker »| Tool | Cas d’usage | Throughput |
|---|---|---|
| Postgres LISTEN/NOTIFY | Petite app monolith, pas de cluster | 10K msg/s |
| Redis Pub/Sub | Petit broker simple | 100K msg/s |
| NATS | Modern, léger, JetStream pour persistence | 1M msg/s |
| RabbitMQ | Standard mature, AMQP, routing complexe | 50K msg/s |
| Apache Kafka | High-throughput, event sourcing, streaming | 1M+ msg/s |
| AWS SQS / Cloud Pub/Sub | Managed, serverless | 10K-100K msg/s |
Choix par défaut 2026 : NATS pour la simplicité ; Kafka si tu fais de l’event sourcing ou du streaming.
CQRS — Command Query Responsibility Segregation
Section intitulée « CQRS — Command Query Responsibility Segregation »Sépare les commandes (qui modifient l’état) des queries (qui lisent).
Commands ──→ [ Write Model (normalized) ] ──events──→ [ Read Model (denormalized) ] ──→ QueriesBénéfice : tu peux dénormaliser le read model (1 table par vue, JOINs faits à l’écriture) → queries ultra-rapides. Le coût : complexité accrue (2 modèles à maintenir).
Quand l’utiliser : ratio reads/writes > 10:1 ET vues très différentes (dashboards, analytics, listings paginés).
Outbox pattern — la cohérence inter-services
Section intitulée « Outbox pattern — la cohérence inter-services »Problème : tu veux garantir que db.save(order) ET eventBus.publish(OrderPlaced) sont atomiques. Si l’un réussit et pas l’autre, ton système est cassé.
Solution outbox :
-- Dans la même transaction que ton write métierBEGIN; INSERT INTO orders (...) VALUES (...); INSERT INTO outbox (event_type, payload) VALUES ('OrderPlaced', '{...}');COMMIT;Puis un worker séparé :
1. SELECT * FROM outbox WHERE published_at IS NULL ORDER BY id LIMIT 1002. Pour chaque row : publier sur le bus + UPDATE outbox SET published_at = NOW()Garantie : événement publié au moins une fois. Les consumers doivent être idempotents.
Saga — orchestration de transactions distribuées
Section intitulée « Saga — orchestration de transactions distribuées »Quand tu as une transaction qui touche N services, tu ne peux pas faire un BEGIN/COMMIT global. Solution : saga = suite d’étapes locales avec compensations en cas d’échec.
1. ChargePayment → si OK : continue → si KO : abort
2. ReserveStock → si OK : continue → si KO : refundPayment ; abort
3. CreateShipment → si OK : commit → si KO : restoreStock ; refundPayment ; abortImplémentation :
- Orchestration : un service central pilote la saga (plus simple, point de bottleneck)
- Chorégraphie : chaque service écoute les events et publie le suivant (plus distribué, plus dur à debug)
6. Auto-évaluation
Section intitulée « 6. Auto-évaluation »Pour aller plus loin
Section intitulée « Pour aller plus loin »- Domain-Driven Design Distilled — Vaughn Vernon (200 pages, lisible en 1 weekend, le résumé de DDD)
- Implementing Domain-Driven Design — Vaughn Vernon (le guide complet, 600 pages)
- Clean Architecture — Robert C. Martin (le livre référence, dogmatique mais utile)
- Designing Data-Intensive Applications — Martin Kleppmann (le standard 2026 pour comprendre les systèmes distribués)
- Microservices Patterns — Chris Richardson (catalogues de patterns avec code Java/Spring)
- Event Storming workshops — méthode pour découvrir les bounded contexts collectivement (Alberto Brandolini)
- microservices.io — référence en ligne avec catalogue de patterns