22. Évoluer une architecture#
Une architecture n’est jamais terminée. Elle évolue avec les besoins du produit, la taille de l’équipe, les contraintes techniques et le contexte économique. La capacité à faire évoluer une architecture de manière contrôlée — sans tout casser, sans arrêter le service — est l’une des compétences les plus précieuses d’un architecte.
Dette technique#
Ward Cunningham, inventeur du terme en 1992, définit la dette technique comme une métaphore financière : prendre des raccourcis aujourd’hui pour livrer plus vite crée une dette qui devra être remboursée plus tard, avec des intérêts. Comme la dette financière, elle peut être utile à court terme et destructrice à long terme si elle s’accumule.
Types de dette technique#
Dette délibérée : un choix conscient de sacrifier la qualité pour gagner du temps. Acceptable si l’équipe comprend le compromis et planifie le remboursement. Cunningham dit que cette forme de dette est parfois la bonne décision.
Dette accidentelle : mauvaises décisions prises sans conscience des conséquences. Code mal structuré, tests insuffisants, documentation absente. Accumulation insidieuse.
Bit rot : le code se dégrade avec le temps même sans être modifié, parce que l’environnement change autour de lui. Dépendances non mises à jour, patterns devenus obsolètes, hypothèses architecturales invalides.
Mesure#
La dette technique est difficile à quantifier précisément, mais des proxies utiles existent : temps moyen pour implémenter une nouvelle fonctionnalité (en augmentation = dette qui croît), taux de bugs en production, temps passé à corriger vs créer, score SQALE dans SonarQube.
La métaphore financière, prise au sérieux
Cunningham insiste : la dette n’est pas mauvaise par définition. Une startup qui livre en 3 mois avec de la dette a une option viable. Une entreprise qui accumule 10 ans de dette sans jamais rembourser finit par ne plus pouvoir livrer quoi que ce soit — c’est la faillite technique.
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
np.random.seed(42)
# Simulation de l'accumulation de dette technique
quarters = np.arange(1, 21) # 20 trimestres = 5 ans
# Scénario A : accumulation sans remboursement
# La dette croît, ralentit les nouvelles features, génère plus de bugs
initial_debt = 10
growth_rate = 0.15 # 15% par trimestre (intérêts sur la dette)
debt_no_payoff = [initial_debt]
velocity_no_payoff = [100] # vélocité initiale (100 = baseline)
for q in range(1, len(quarters)):
new_debt = debt_no_payoff[-1] * (1 + growth_rate)
# La vélocité diminue avec la dette accumulée
new_velocity = max(10, 100 * np.exp(-0.03 * new_debt))
debt_no_payoff.append(new_debt)
velocity_no_payoff.append(new_velocity)
# Scénario B : remboursement régulier (20% du budget en refactoring)
debt_with_payoff = [initial_debt]
velocity_with_payoff = [100]
for q in range(1, len(quarters)):
# Sans remboursement : croissance
raw_debt = debt_with_payoff[-1] * (1 + growth_rate)
# Remboursement : réduction de 25% de la dette par trimestre grâce au refactoring
payoff_rate = 0.25 if q > 4 else 0.10 # l'équipe commence à investir dès Q4
new_debt = raw_debt * (1 - payoff_rate)
new_velocity = max(60, 100 * np.exp(-0.03 * max(0, new_debt - 5)))
debt_with_payoff.append(new_debt)
velocity_with_payoff.append(new_velocity)
fig, axes = plt.subplots(2, 1, figsize=(13, 9), sharex=True)
ax1 = axes[0]
ax1.plot(quarters, debt_no_payoff, 'o-', color='red', linewidth=2.5,
markersize=6, label='Sans remboursement')
ax1.plot(quarters, debt_with_payoff, 's-', color='green', linewidth=2.5,
markersize=6, label='Avec remboursement (refactoring régulier)')
ax1.fill_between(quarters, debt_no_payoff, debt_with_payoff, alpha=0.15, color='orange',
label='Écart de dette')
ax1.axvline(4, color='purple', linestyle=':', linewidth=1.5, alpha=0.7,
label='Début investissement refactoring')
ax1.set_ylabel("Niveau de dette technique (UA)")
ax1.set_title("Accumulation de la dette technique dans le temps", fontsize=12)
ax1.legend(fontsize=9)
ax2 = axes[1]
ax2.plot(quarters, velocity_no_payoff, 'o-', color='red', linewidth=2.5,
markersize=6, label='Vélocité (sans remboursement)')
ax2.plot(quarters, velocity_with_payoff, 's-', color='green', linewidth=2.5,
markersize=6, label='Vélocité (avec remboursement)')
ax2.axhline(100, color='gray', linestyle='--', linewidth=1, alpha=0.5, label='Vélocité initiale')
ax2.axhline(50, color='orange', linestyle=':', linewidth=1.5, alpha=0.7,
label='Seuil critique (50%)')
ax2.set_xlabel("Trimestre")
ax2.set_ylabel("Vélocité d'équipe (%)")
ax2.set_title("Impact de la dette technique sur la vélocité", fontsize=12)
ax2.legend(fontsize=9)
ax2.set_ylim(0, 115)
ax2.set_xticks(quarters)
ax2.set_xticklabels([f'Q{q}' for q in quarters], fontsize=8)
plt.suptitle("Dette technique : accumulation et impact sur la vélocité", fontsize=13, fontweight='bold')
plt.savefig("tech_debt.png", dpi=100, bbox_inches='tight')
plt.show()
print(f"\nQ20 dette sans remboursement : {debt_no_payoff[-1]:.0f} UA")
print(f"Q20 dette avec remboursement : {debt_with_payoff[-1]:.0f} UA")
print(f"Q20 vélocité sans remboursement : {velocity_no_payoff[-1]:.0f}%")
print(f"Q20 vélocité avec remboursement : {velocity_with_payoff[-1]:.0f}%")
Q20 dette sans remboursement : 142 UA
Q20 dette avec remboursement : 1 UA
Q20 vélocité sans remboursement : 10%
Q20 vélocité avec remboursement : 100%
Fitness Functions#
Les fitness functions, concept introduit par Neal Ford, Rebecca Parsons et Patrick Kua dans Building Evolutionary Architectures, sont des tests automatisés de propriétés architecturales.
L’idée : tout comme les tests unitaires garantissent que le code se comporte correctement, les fitness functions garantissent que l’architecture respecte ses contraintes — et elles s’exécutent dans le pipeline CI/CD.
Exemples concrets#
Dépendances cycliques : une fitness function qui échoue si deux modules ont des dépendances circulaires (ArchUnit en Java, import-linter en Python).
Couplage entre bounded contexts : vérifier qu’un service ne dépend pas directement d’un autre service en dehors de son domaine — uniquement via des interfaces définies.
Temps de démarrage : la fitness function mesure le temps de démarrage du service et échoue s’il dépasse 5 secondes.
Couverture de tests : une règle architecturale qui interdit de merger si la couverture descend sous 80%.
Règles de nommage : les controllers ne doivent pas dépendre directement des entités de base de données — uniquement des services.
Fitness functions vs tests
Les tests vérifient le comportement du code. Les fitness functions vérifient les propriétés structurelles de l’architecture. Les deux sont complémentaires. Une fitness function qui détecte une dépendance cyclique protège l’architecture des dérives accidentelles sur le long terme.
Strangler Fig Pattern#
Le Strangler Fig Pattern (Martin Fowler) tire son nom d’un figuier tropical qui pousse autour d’un arbre hôte, le remplace progressivement, et finit par le supplanter complètement — sans jamais « couper » l’arbre d’un coup.
Migration incrémentale sans big bang#
La migration big bang — réécrire l’intégralité du système puis basculer — est l’une des pratiques les plus risquées de l’ingénierie logicielle. Elle prend des années, le système existant continue d’évoluer pendant ce temps, et le risque de régression est immense.
Le Strangler Fig procède différemment : une façade (proxy, API gateway, ou simple routeur) intercepte toutes les requêtes. Les nouvelles fonctionnalités sont implémentées dans le nouveau système. Les requêtes correspondantes sont routées vers le nouveau système. L’ancien système traite les requêtes restantes.
Progressivement, le routage bascule vers le nouveau système, fonctionnalité par fonctionnalité. L’ancien système rétrécit jusqu’à pouvoir être éteint.
Métriques de migration#
Le pourcentage du trafic traité par le nouveau système est la métrique principale. Elle doit augmenter de manière monotone — si une migration régresse, c’est un signal d’alarme.
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import seaborn as sns
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
np.random.seed(7)
# Simulation Strangler Fig : migration d'un monolithe
months = np.arange(1, 25) # 24 mois de migration
# Composants migrés progressivement
components = [
'Auth & IAM',
'Catalogue produits',
'Gestion commandes',
'Paiement',
'Notifications',
'Reporting',
'Admin panel',
'Monolithe (résiduel)',
]
# Trafic initial et progression de migration
# Chaque composant migre selon un planning
migration_schedule = {
'Auth & IAM': (1, 4), # mois de début, durée
'Catalogue produits': (3, 5),
'Gestion commandes': (5, 6),
'Paiement': (8, 4),
'Notifications': (10, 3),
'Reporting': (12, 4),
'Admin panel': (15, 5),
}
# Part de trafic de chaque composant
traffic_share = {
'Auth & IAM': 0.08,
'Catalogue produits': 0.25,
'Gestion commandes': 0.20,
'Paiement': 0.15,
'Notifications': 0.07,
'Reporting': 0.05,
'Admin panel': 0.05,
'Monolithe (résiduel)': 0.15,
}
def migration_progress(month, start, duration):
"""Retourne le pourcentage migré pour un composant."""
if month < start:
return 0.0
if month >= start + duration:
return 1.0
# Progression sigmoïde
t = (month - start) / duration
return 1 / (1 + np.exp(-10 * (t - 0.5)))
fig, axes = plt.subplots(1, 2, figsize=(16, 7))
# Stacked bar : trafic par composant dans le nouveau système
ax1 = axes[0]
colors_comp = sns.color_palette("muted", len(components))
traffic_new = np.zeros((len(components), len(months)))
for i, comp in enumerate(components):
if comp in migration_schedule:
start, duration = migration_schedule[comp]
for j, m in enumerate(months):
traffic_new[i, j] = migration_progress(m, start, duration) * traffic_share[comp]
# Le monolithe résiduel ne migre pas dans ce scénario
# Part du monolithe = ce qui n'est pas encore migré
monolith_traffic = np.ones(len(months)) - traffic_new.sum(axis=0) + traffic_share['Monolithe (résiduel)']
bottom = np.zeros(len(months))
for i, comp in enumerate(components[:-1]):
ax1.bar(months, traffic_new[i], bottom=bottom, color=colors_comp[i],
label=comp, alpha=0.85, edgecolor='white', linewidth=0.5)
bottom += traffic_new[i]
ax1.bar(months, monolith_traffic, bottom=bottom, color='#bdc3c7',
label='Monolithe (résiduel)', alpha=0.85, edgecolor='white', linewidth=0.5)
ax1.set_xlabel("Mois")
ax1.set_ylabel("Part du trafic")
ax1.set_title("Migration Strangler Fig\nTrafic routé par composant", fontsize=12)
ax1.legend(fontsize=7.5, loc='center left', bbox_to_anchor=(1, 0.5))
ax1.set_xlim(0.5, 24.5)
ax1.set_ylim(0, 1.1)
ax1.set_yticks(np.arange(0, 1.1, 0.2))
ax1.set_yticklabels([f'{int(v*100)}%' for v in np.arange(0, 1.1, 0.2)])
# Courbe de progression globale
ax2 = axes[1]
total_migrated = traffic_new.sum(axis=0)
monolith_pct = (1 - total_migrated) * 100
ax2.fill_between(months, total_migrated * 100, 100, alpha=0.3, color='#bdc3c7',
label='Monolithe (% trafic)')
ax2.fill_between(months, 0, total_migrated * 100, alpha=0.4, color='steelblue',
label='Nouveau système (% trafic)')
ax2.plot(months, total_migrated * 100, color='steelblue', linewidth=2.5)
ax2.plot(months, monolith_pct, color='gray', linewidth=2, linestyle='--')
# Jalons
milestones = [50, 80, 95]
for pct in milestones:
idx = next((j for j, v in enumerate(total_migrated * 100) if v >= pct), None)
if idx is not None:
ax2.axvline(months[idx], color='orange', linestyle=':', linewidth=1.5, alpha=0.8)
ax2.text(months[idx] + 0.3, pct - 8, f'{pct}%\n(M{months[idx]})',
fontsize=7.5, color='darkorange')
ax2.set_xlabel("Mois")
ax2.set_ylabel("% du trafic")
ax2.set_title("Progression globale de la migration\nVers le nouveau système", fontsize=12)
ax2.legend(fontsize=9)
ax2.set_xlim(0.5, 24.5)
ax2.set_ylim(0, 105)
plt.suptitle("Strangler Fig Pattern — Migration incrémentale sur 24 mois", fontsize=13, fontweight='bold')
plt.savefig("strangler_fig.png", dpi=100, bbox_inches='tight')
plt.show()
Migration monolithe → microservices#
La migration d’un monolithe vers des microservices est l’une des transformations architecturales les plus communes et les plus risquées.
Étapes recommandées#
Identifier les bounded contexts : utiliser le DDD stratégique pour identifier les frontières naturelles. Les bounded contexts deviennent les candidats à l’extraction.
Commencer par les services les moins couplés : extraire d’abord ce qui a peu de dépendances avec le reste. Le reporting, les notifications, les services auxiliaires.
Strangler Fig pour chaque extraction : ne jamais couper le monolithe d’un coup. Extraire un service, le déployer en parallèle, basculer le trafic progressivement.
Synchroniser les données pendant la transition : pendant la coexistence du monolithe et du nouveau service, les deux partagent (ou synchronisent) les données. C’est la phase la plus délicate.
Le piège du monolithe distribué#
Le distributed monolith est le résultat le plus fréquent d’une mauvaise migration : des services séparés qui se parlent en synchrone pour tout, qui partagent une base de données commune, qui ne peuvent pas être déployés indépendamment. On a toutes les complexités des microservices sans aucun de leurs bénéfices.
Savoir s’arrêter
La migration vers les microservices n’est pas un objectif en soi. Si une équipe de 5 développeurs gère un monolithe bien structuré et livre de la valeur, le migrer vers des microservices ajoutera de la complexité sans bénéfice. La migration se justifie par des besoins réels : scalabilité indépendante, ownership par équipe, déploiements indépendants.
Evolutionary Architecture#
L”Evolutionary Architecture (Ford, Parsons, Kua) défend une architecture qui peut évoluer de manière incrémentale et guidée, sans grandes ruptures.
« Last responsible moment »#
Retarder les décisions irréversibles aussi longtemps que possible. Plus une décision est prise tôt, moins on dispose d’information. Quand la décision doit absolument être prise, on dispose de toute l’information disponible.
Décisions réversibles vs irréversibles#
Certaines décisions sont facilement réversibles : changer un framework frontend, renommer un service, ajouter un index. D’autres sont extrêmement coûteuses à changer : le choix du moteur de base de données, le protocole de communication entre services, le format de stockage des événements.
Les décisions irréversibles requièrent plus de délibération, plus de prototypage, plus de consultation. Les décisions réversibles peuvent être prises rapidement et changées si elles s’avèrent mauvaises.
Incremental change#
Préférer les petits changements fréquents aux grandes transformations rares. Un déploiement de 10 lignes de code est testable, rollbackable, et ne crée pas de risque. Une refonte de 3 mois est difficile à valider et catastrophique si elle échoue.
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import seaborn as sns
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
# Radar de maturité architecturale sur 8 dimensions
categories = [
'Tests\nautomatisés',
'Observabilité',
'CI/CD',
'Sécurité\nby design',
'Scalabilité',
'Documentation\n(ADR)',
'Couplage\nfaible',
'Fitness\nFunctions',
]
n_cat = len(categories)
angles = np.linspace(0, 2 * np.pi, n_cat, endpoint=False).tolist()
angles += angles[:1]
# Scores : état actuel vs objectif
current_scores = [3, 2, 4, 2, 3, 1, 3, 1]
target_scores = [5, 4, 5, 4, 5, 4, 4, 3]
current_scores += current_scores[:1]
target_scores += target_scores[:1]
fig, axes = plt.subplots(1, 2, figsize=(16, 7))
ax_radar = plt.subplot(121, polar=True)
colors = sns.color_palette("muted", 3)
ax_radar.plot(angles, target_scores, 'o--', linewidth=2, color='steelblue',
alpha=0.7, label='Objectif (dans 12 mois)')
ax_radar.fill(angles, target_scores, alpha=0.1, color='steelblue')
ax_radar.plot(angles, current_scores, 'o-', linewidth=2.5, color='darkorange',
label='Niveau actuel')
ax_radar.fill(angles, current_scores, alpha=0.2, color='darkorange')
ax_radar.set_xticks(angles[:-1])
ax_radar.set_xticklabels(categories, fontsize=9)
ax_radar.set_ylim(0, 5.5)
ax_radar.set_yticks([1, 2, 3, 4, 5])
ax_radar.set_yticklabels(['1', '2', '3', '4', '5'], fontsize=7)
ax_radar.set_title("Radar de maturité architecturale\n(Score 1-5)", fontsize=11,
pad=15, fontweight='bold')
ax_radar.legend(loc='upper right', bbox_to_anchor=(1.4, 1.1), fontsize=9)
# Timeline décisions réversibles vs irréversibles
ax2 = axes[1]
ax2.set_xlim(0, 14)
ax2.set_ylim(-0.5, 7.5)
ax2.axis('off')
ax2.set_title("Décisions architecturales : réversibles vs irréversibles\n(Timeline projet)", fontsize=11, fontweight='bold')
decisions = [
# (mois_début, durée, label, réversible, y_pos)
(0, 3, "Choix BDD PostgreSQL", False, 7),
(0, 2, "Choix broker Kafka", False, 6),
(1, 6, "Framework API (FastAPI)", True, 5),
(2, 2, "Format events (JSON)", False, 4),
(3, 8, "Stratégie de cache (Redis)", True, 3),
(4, 4, "Découpage bounded contexts", False, 2),
(6, 6, "Naming conventions", True, 1),
(8, 4, "Stratégie d'index DB", True, 0),
]
colors_rev = {'reversible': '#27ae60', 'irreversible': '#e74c3c'}
for start, dur, label, is_irreversible, ypos in decisions:
color = '#e74c3c' if is_irreversible else '#27ae60'
alpha = 0.85
bar = mpatches.FancyBboxPatch((start, ypos - 0.3), dur, 0.6,
boxstyle="round,pad=0.05",
facecolor=color, edgecolor='white',
linewidth=1.5, alpha=alpha)
ax2.add_patch(bar)
ax2.text(start + dur/2, ypos, label, ha='center', va='center',
fontsize=7.5, color='white', fontweight='bold')
legend_elements = [
mpatches.Patch(color='#e74c3c', alpha=0.85, label='Décision irréversible (coûteuse à changer)'),
mpatches.Patch(color='#27ae60', alpha=0.85, label='Décision réversible (facile à changer)'),
]
ax2.legend(handles=legend_elements, loc='lower right', fontsize=8)
ax2.set_xticks(range(0, 15))
ax2.set_xticklabels([f'M{i}' for i in range(0, 15)], fontsize=8)
ax2.xaxis.set_visible(True)
# Réactiver l'axe X
for spine in ax2.spines.values():
spine.set_visible(False)
ax2.set_xticks(range(0, 14))
ax2.tick_params(axis='x', which='both', bottom=False, labelbottom=True)
plt.suptitle("Evolutionary Architecture : maturité et décisions", fontsize=13, fontweight='bold')
plt.savefig("evolutionary_architecture.png", dpi=100, bbox_inches='tight')
plt.show()
Architecture review#
Une architecture review est un processus de validation d’une décision architecturale par un groupe de pairs. Elle peut être légère ou lourde selon la portée de la décision.
RFC process (Request for Comments)#
Le RFC process est une pratique empruntée à l’IETF et popularisée dans les grandes entreprises tech (Stripe, GitHub, Rust). Une décision architecturale significative est rédigée sous forme de document structuré (contexte, problème, solutions envisagées, solution retenue, conséquences) et soumise à commentaires pendant une période définie (1-2 semaines).
Avantages : documentation systématique des décisions, alignement de l’équipe, perspectives multiples, trace historique. Le RFC devient un ADR (Architecture Decision Record) une fois approuvé.
Lightweight vs heavyweight#
Un RFC de 2 pages pour une décision qui touche un seul service est disproportionné. Une réunion informelle de 30 minutes avec les personnes concernées suffit. À l’inverse, décider du choix de la base de données pour la plateforme entière sans processus structuré est imprudent.
La règle : la rigueur du processus de review est proportionnelle au coût du reversement de la décision.
Pairing avec les équipes#
L’architecte qui décrète des standards sans travailler avec les équipes perd rapidement leur crédibilité. Le pair programming et les reviews de code architecture sont les outils pour maintenir le contact avec la réalité du terrain et valider que les décisions architecturales sont applicables.
Prochaines étapes#
L’architecture logicielle est un domaine qui évolue rapidement mais dont les fondamentaux restent stables. Quelques ressources pour approfondir :
Livres fondamentaux#
Domain-Driven Design (Eric Evans) : la référence absolue sur la modélisation du domaine. Dense mais incontournable pour tout architecte travaillant sur des domaines complexes.
Patterns of Enterprise Application Architecture (Martin Fowler) : catalogue de patterns pour les applications d’entreprise. Repository, Unit of Work, Identity Map, Service Layer — les patterns qui structurent la couche de persistance et applicative.
Building Microservices (Sam Newman, 2e édition) : la référence pratique sur les microservices. Communication, données, déploiements, monitoring — tout y est.
Clean Architecture (Robert C. Martin) : les principes SOLID appliqués à l’architecture. Dependency inversion, ports & adapters, indépendance des frameworks.
Building Evolutionary Architectures (Ford, Parsons, Kua) : fitness functions, evolutionary design, guided change. La référence pour architecturer pour le changement.
Release It! (Michael Nygard, 2e édition) : patterns de résilience en production. Circuit breaker, bulkhead, timeouts — la bible de la stabilité des systèmes distribués.
Communautés et pratiques#
Architecture katas : exercices d’architecture proposés par Ted Neward. Pratiquer l’architecture sur des cas fictifs avec des contraintes imposées est l’un des meilleurs moyens de progresser.
Architecture haiku : décrire une architecture en un paragraphe. Exercice de clarté et de synthèse.
Exploration des systèmes existants : lire les articles d’architecture des grandes entreprises tech (Netflix, Airbnb, Stripe, Uber engineering blogs) donne une perspective sur les choix réels et leurs conséquences.
Résumé#
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import seaborn as sns
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
np.random.seed(99)
# Simulation : Strangler Fig avec métriques de santé
months = np.arange(1, 25)
# Modélisation de la dette technique pendant la migration
debt_during_migration = (
50 + 10 * np.exp(-0.1 * months) # réduction progressive par le refactoring
+ 5 * np.sin(months * 0.5) # oscillations dues aux nouvelles features
+ np.random.normal(0, 2, len(months))
)
debt_during_migration = np.clip(debt_during_migration, 10, 80)
# Vélocité
velocity_during_migration = (
60 + 25 * (1 - np.exp(-0.15 * months)) # remonte progressivement
+ np.random.normal(0, 3, len(months))
)
velocity_during_migration = np.clip(velocity_during_migration, 40, 100)
# Lead time for changes (proxy qualité architecture)
lead_time = (
30 * np.exp(-0.1 * months) + 5 # améliore avec le temps
+ np.random.normal(0, 1, len(months))
)
lead_time = np.clip(lead_time, 3, 35)
fig, axes = plt.subplots(3, 1, figsize=(13, 11), sharex=True)
ax1 = axes[0]
ax1.plot(months, debt_during_migration, 'o-', color='red', linewidth=2,
markersize=4, label='Dette technique estimée')
ax1.fill_between(months, debt_during_migration, alpha=0.2, color='red')
ax1.axhline(30, color='orange', linestyle='--', linewidth=1.5, alpha=0.7,
label='Seuil cible')
ax1.set_ylabel("Niveau de dette (UA)")
ax1.set_title("Évolution de la dette technique pendant la migration", fontsize=11)
ax1.legend(fontsize=9)
ax1.set_ylim(0, 90)
ax2 = axes[1]
ax2.plot(months, velocity_during_migration, 's-', color='steelblue', linewidth=2,
markersize=4, label="Vélocité d'équipe (%)")
ax2.fill_between(months, velocity_during_migration, alpha=0.2, color='steelblue')
ax2.axhline(85, color='green', linestyle='--', linewidth=1.5, alpha=0.7,
label='Objectif vélocité (85%)')
ax2.set_ylabel("Vélocité (%)")
ax2.set_title("Vélocité de l'équipe pendant la migration", fontsize=11)
ax2.legend(fontsize=9)
ax2.set_ylim(30, 110)
ax3 = axes[2]
ax3.plot(months, lead_time, '^-', color='purple', linewidth=2,
markersize=4, label='Lead time (jours)')
ax3.fill_between(months, lead_time, alpha=0.2, color='purple')
ax3.axhline(7, color='green', linestyle='--', linewidth=1.5, alpha=0.7,
label='Objectif : ≤ 7 jours')
ax3.set_xlabel("Mois de migration")
ax3.set_ylabel("Lead time (jours)")
ax3.set_title("Lead time for changes (DORA metric)", fontsize=11)
ax3.legend(fontsize=9)
ax3.set_ylim(0, 40)
ax3.set_xlim(0.5, 24.5)
ax3.set_xticks(months)
ax3.set_xticklabels([f'M{m}' for m in months], fontsize=7.5)
plt.suptitle("Métriques d'une migration architecturale réussie (24 mois)", fontsize=13, fontweight='bold')
plt.savefig("migration_metrics.png", dpi=100, bbox_inches='tight')
plt.show()
Évoluer une architecture est un exercice permanent d’équilibre entre continuité et changement.
Pratique |
Problème résolu |
Outil / Pattern |
|---|---|---|
Fitness functions |
Dérives architecturales silencieuses |
ArchUnit, import-linter, CI/CD |
Strangler Fig |
Migration sans big bang |
Façade, routing progressif |
RFC process |
Décisions non documentées, non alignées |
Document structuré, commentaires async |
Evolutionary arch. |
Rigidité architecturale |
Last responsible moment, small steps |
Radar de maturité |
Visibilité sur les axes d’amélioration |
Revue périodique, scoring collectif |
Règles pratiques :
La dette technique est un choix. Elle devient un problème quand elle est accidentelle et non gérée.
Le Strangler Fig est presque toujours préférable au big bang. La clé est une façade bien conçue dès le début.
Les fitness functions automatisent la gouvernance architecturale — elles rendent les contraintes exécutables, pas juste recommandées.
Distinguer les décisions réversibles des irréversibles et ajuster le niveau de délibération en conséquence.
L’architecture n’est pas un état final — c’est un processus continu d’adaptation guidée par les besoins réels.