Votre monolithe a dix ans, chaque version exige un long cycle de régression, et quelqu'un propose une réécriture : construire le nouveau système à côté, atteindre la parité fonctionnelle, basculer un week-end. Pendant ce temps, l'ancien système continue d'évoluer et la date de bascule glisse. Martin Fowler écrit que ses collègues et lui ont vu ce plan « partir en flammes la plupart du temps ».
Le pattern strangler fig remplace plutôt le système morceau par morceau. Une façade devant le monolithe route chaque requête vers l'ancien code ou vers un nouveau service, et les capacités migrent une à une jusqu'à ce que plus rien ne soit routé vers le monolithe. Le routage est la partie facile. La propriété des données, la preuve de parité et la fin du chantier sont les parties difficiles.
Cet article couvre tout cela, avec un diagramme d'une migration en cours.
D'où vient le nom
En 2001, Fowler a vu des figuiers étrangleurs dans la forêt tropicale du Queensland : des lianes qui germent sur un arbre hôte, s'enracinent dans le sol et peuvent survivre à l'arbre, laissant le figuier à sa forme. En juin 2004, il les a utilisés comme métaphore du remplacement progressif d'un système legacy. En avril 2019, il a renommé l'article de « Strangler Application » en « Strangler Fig Application », pour renvoyer à la plante, et l'a réécrit en août 2024. La version actuelle insiste sur ce que les équipes refusent : l'architecture transitoire, du code que vous écrivez uniquement pour que l'ancien et le nouveau coexistent.
La façade fait le routage
La façade est un reverse proxy ou une passerelle d'API devant le monolithe : NGINX, Envoy, une passerelle d'API cloud ou un load balancer avec des règles de chemin. Au départ, elle envoie tout au monolithe. Ensuite, chaque étape de migration est un changement de routage. Routez selon :
- Le chemin :
/catalog/*va au service catalogue. - La méthode :
GET /orders/*avantPOST /orders, pour que les lectures migrent d'abord. - La part : un hash d'un identifiant de session ou d'utilisateur garde chaque utilisateur du même côté.
- L'en-tête ou le tenant : les utilisateurs internes ou un client pilote passent en premier.
# upstream blocks catalog, orders and monolith defined elsewhere
split_clients "${cookie_session}" $orders_read {
25% orders; # canary share for reads
* monolith;
}
map $request_method $orders_target {
GET $orders_read;
default monolith; # order writes stay in the monolith
}
server {
location /catalog/ { proxy_pass http://catalog; }
location /orders/ { proxy_pass http://$orders_target; }
location / { proxy_pass http://monolith; }
}
Le retour arrière tient en une modification : pointez la ligne à 25 % vers monolith (nginx refuse une entrée à 0 %). Hachez une clé que toute requête porte : les requêtes sans le cookie atterrissent toutes dans le même bucket.
Gardez la façade fine : routage, TLS, authentification et journalisation, aucune règle métier. Elle se trouve sur chaque requête, donc exécutez plusieurs instances et mesurez la latence qu'elle ajoute. Le diagramme d'architecture d'une passerelle d'API explique ce qui a sa place dans une passerelle.
Les quatre phases d'une migration
| Phase | La façade route | Données | Terminé quand |
|---|---|---|---|
| 0. La façade d'abord | Tout vers le monolithe | Inchangées | Façade en production, latence ajoutée mesurée |
| 1. Première capacité | Une route, comme /catalog/*, vers un nouveau service |
Le service lit les tables du monolithe ou une copie CDC, puis les siennes | Route à 100 % sur un cycle métier complet |
| 2. L'essentiel du trafic sur les nouveaux services | La plupart des routes ; le monolithe garde une liste connue | Chaque service possède ses tables | Ce qui reste tient sur une page |
| 3. Monolithe retiré | Rien vers le monolithe | Base legacy archivée, puis supprimée | Monolithe éteint |
La page du pattern Strangler Fig de Microsoft ajoute une dernière étape : supprimer la façade, ou la garder comme adaptateur pour les clients legacy.
Choisir quoi extraire en premier
Les bons premiers candidats ont trois traits :
- Une couture nette, selon Michael Feathers un endroit où l'on peut modifier le comportement sans éditer à cet endroit : un préfixe d'URL, une file, une frontière de module.
- Un taux de changement élevé, pour que la migration soit vite rentable. Le contrôle de version le montre.
- Un faible couplage : peu de jointures vers d'autres tables, peu d'appels de retour vers le monolithe.
git log --since="12 months ago" --format= --name-only -- src/ \
| sort | uniq -c | sort -rn | head -20
Un classement pour une boutique en ligne :
| Candidat | Couture | Taux de changement | Couplage | Verdict |
|---|---|---|---|---|
| Navigation du catalogue | /catalog/* |
Élevé | Faible | En premier |
| Historique des commandes | GET /orders/* |
Moyen | Moyen | En deuxième, lectures d'abord |
| Paiement | Appelle les prix, le stock et les paiements | Élevé | Élevé | Après ses dépendances |
| Rapports de nuit | SQL sur tout le schéma | Faible | Très élevé | En dernier, ou un flux vers un entrepôt |
Extrayez par capacité métier, pas par couche technique. Un « service d'accès aux données » partagé ne déplace rien et couple tout.
Déplacer les données
Le routage déplace les requêtes, pas les données, et c'est sur les données que les migrations s'enlisent. Décidez à chaque étape qui possède chaque table.
| Stratégie | Fonctionnement | À utiliser pour | Risque |
|---|---|---|---|
| Base partagée | Le nouveau service utilise les tables du monolithe | Les premières semaines | Les changements de schéma du monolithe le cassent |
| Change data capture | Un connecteur diffuse les changements de lignes depuis le journal du monolithe | Déplacer les lectures d'abord | Retard, schémas legacy, flux à sens unique |
| Une base par service | Le service possède ses tables ; les autres appellent son API | L'état final | Les jointures entre services deviennent des appels d'API |
| Doubles écritures depuis le code | L'application écrit dans les deux bases | Rien | Un crash entre les deux écritures les désynchronise |
Les recommandations de Microsoft donnent l'ordre habituel : le nouveau service démarre sur les tables du monolithe, puis obtient sa propre base, chargée une fois et tenue à jour par CDC, et devient le système de référence après validation. Tant que les tables legacy existent, le retour arrière est un changement de route ; ensuite, c'est une restauration.
Évitez les doubles écritures : un crash entre deux écritures séparées laisse les bases en désaccord, et des mises à jour concurrentes peuvent arriver dans un ordre différent. Faites d'une seule transaction la source de chaque changement, via le CDC ou un transactional outbox. Le diagramme de change data capture couvre Debezium et les slots de réplication.
La couche anti-corruption
Le modèle du monolithe fuit : codes de statut d'une lettre, champs de facturation et de livraison dans une seule table, règles dans des procédures stockées. Un service qui consomme ces lignes hérite du modèle que vous quittez. Une couche anti-corruption, un pattern du Domain-Driven Design d'Eric Evans (2003), traduit à la frontière :
- Legacy vers nouveau : des événements CDC portant des lignes legacy deviennent des commandes dans le nouveau modèle.
- Nouveau vers legacy : le service de commandes lit les données clients du monolithe via un adaptateur.
- Monolithe vers nouveau : une fois le catalogue migré, le code du monolithe appelle l'API du catalogue au lieu de ses anciennes tables.
Gardez les règles métier hors de la couche. Dans un service hexagonal, elle vit dans les adaptateurs, si bien que le cœur ne voit jamais les types legacy.
Prouver la parité avant de déplacer une route
Chaque route suit les mêmes étapes :
- Shadow. La façade copie les requêtes vers le nouveau service et jette ses réponses, comme le font le module mirror de NGINX et le request mirroring d'Envoy. Ne dupliquez que les lectures : un
POST /ordersdupliqué crée une seconde commande. - Comparer. Comparez les deux réponses après avoir normalisé horodatages, identifiants et ordre. Diffy de Twitter, publié en open source en 2015, exécute deux copies de l'ancien code et ignore comme du bruit ce qui diffère entre elles. À l'intérieur du monolithe, la bibliothèque Scientist de GitHub (Ruby, avec des portages vers d'autres langages) exécute côte à côte l'ancien et le nouveau chemin et renvoie le résultat de l'ancien.
- Canary. Déplacez 1 %, 5 %, 25 %, 50 %, puis 100 % du trafic réel, comparez le taux d'erreur et la latence p99 avec ceux du monolithe pour la même route, et tenez chaque palier pendant un pic quotidien. Le diagramme des stratégies de déploiement couvre la mécanique.
- Tenir. À 100 %, gardez l'ancien chemin déployable et ses données à jour à travers des événements comme une clôture de fin de mois.
- Supprimer l'ancien code, ses tables et la route.
Rebasculer une route ne fonctionne que tant que le monolithe a des données à jour. Une fois que le nouveau service possède les écritures, renvoyez-les par CDC inverse ou par événements, sinon un retour arrière les perd.
Comment les migrations strangler fig échouent
- Ne jamais finir. Les 80 % faciles migrent ; les rapports, les traitements batch et les écrans d'administration restent, et vous exploitez deux systèmes. Comptez ce qui reste à chaque sprint et fixez une date d'extinction.
- Un monolithe distribué. Des services qui partagent des tables, se déploient ensemble ou s'appellent en longues chaînes synchrones. Le test : un seul écrivain par table.
- La façade comme goulot d'étranglement : de la logique métier dans la passerelle, ou une seule instance qui porte tout le trafic.
- Des consommateurs cachés. Des rapports et d'autres équipes lisent directement la base du monolithe, sans que la façade les voie. Vérifiez les journaux de connexion avant de déplacer une table.
- La parité fonctionnelle comme objectif. Fowler note qu'une grande partie du comportement legacy « n'est pas vraiment voulue ». Patterns of Legacy Displacement traite de ce piège et d'autres.
Le diagramme de migration strangler fig
Contenu du diagramme
- Customers
- Facade (API gateway)
- Catalog service
- Orders service
- Legacy monolith
- Catalog DB
- Orders DB
- Monolith DB
- Debezium + Kafka
- Anti-corruption layer
- HTTPS
- /catalog/* · 100%
- GET /orders/* · 25%
- everything else + 75% of GET /orders
- reads, writes
- reads
- WAL changes
- order row events
- upserts in the new model
Comment le lire :
- De gauche à droite : les clients appellent la façade, qui répartit le trafic par route ; les bases de données sont à droite.
- Vert, migré : tout
/catalog/*va au service catalogue, qui possède ses données. - Ambre, canary : 25 % de
GET /orders/*vont au service de commandes. Les écritures et les autres lectures vont toujours au monolithe. - Gris, legacy : le monolithe sert le reste et possède toujours les données de commandes.
- Violet, en pointillés : Debezium diffuse les changements de commandes depuis le WAL du monolithe via Kafka, et la couche anti-corruption les convertit vers le nouveau modèle.
- La table de routage liste cible, part et phase pour chaque route, si bien que le retour arrière tient en une modification.
Dessiner clairement les migrations strangler fig
- Un diagramme par phase, ou un titre daté ; gardez les anciens pour montrer la direction.
- Étiquetez les routes avec le chemin et la part, comme « GET /orders/* · 25 % ».
- Montrez la propriété des données : le système de référence et le sens de chaque synchro.
- Colorez par statut (migré, canary, legacy), pas par équipe.
- Laissez de côté les détails internes. C'est une carte de migration, pas un diagramme de composants.
- Dessinez les consommateurs cachés, comme un rapport de nuit qui lit la base du monolithe.
Le diagramme d'architecture microservices montre comment garder l'état final lisible.
Le construire dans Archboard
- Connectez-vous avec Google ou GitHub et créez un diagramme.
- Ajoutez une Personne depuis le pack C4 Model pour les clients et une Passerelle API depuis le pack Cloud pour la façade.
- Ajoutez trois nœuds Conteneur depuis le pack C4 Model : Catalog service, Orders service et Legacy monolith. Colorez-les en vert, ambre et gris.
- Ajoutez un nœud Postgres depuis le pack Données à droite de chaque conteneur.
- Entre la base du monolithe et la base des commandes, empilez un nœud Kafka du pack Données (Debezium + Kafka) et un Composant du pack C4 Model (Anti-corruption layer), reliés vers le haut par des arêtes en pointillés.
- Tracez les routes depuis la façade, étiquetées avec le chemin et la part, et ajoutez au-dessus de chaque conteneur une étiquette d'état Texte du pack Base.
- Ajoutez un bloc Markdown du pack Base avec la table de routage.
Si votre carte des routes vit dans un flowchart Mermaid, le convertisseur Mermaid en fait un premier brouillon modifiable.
L'exemple utilise 11 nœuds de diagramme et reste dans les limites de l'offre Free.
FAQ
Qu'est-ce que le pattern strangler fig ? Une façon de remplacer un système legacy progressivement : une façade route chaque requête vers l'ancien code ou vers un nouveau service, les capacités migrent une à une, et l'ancien système est éteint quand plus rien n'y est routé.
Pourquoi l'appelle-t-on pattern strangler fig ? Martin Fowler l'a nommé en 2004 d'après les figuiers étrangleurs vus dans le Queensland, qui poussent autour d'un arbre hôte et peuvent le remplacer. Il a ajouté Fig au nom en 2019 pour souligner la plante.
Comment gérer la base de données dans une migration strangler fig ? Par étapes. Le nouveau service lit d'abord les tables du monolithe, puis obtient sa propre base alimentée par change data capture, et prend en charge les écritures après validation. Évitez les doubles écritures depuis le code applicatif.
Quelle est la différence entre strangler fig et branch by abstraction ? Le strangler fig route les requêtes à la périphérie du système. Branch by abstraction agit dans le code : une interface permet à l'ancienne et à la nouvelle implémentation de coexister jusqu'à ce que chaque appelant ait basculé. Beaucoup de migrations utilisent les deux.
Quand ne faut-il pas utiliser le pattern strangler fig ? Quand les requêtes vers l'ancien système ne peuvent pas être interceptées, quand le système est assez petit pour être remplacé d'un coup, ou quand il doit être éteint rapidement.