Chapitre 4 — Documentation et ADR#
Une architecture non documentée est une architecture qui disparaît. Quand les personnes qui l’ont conçue quittent l’équipe, leur connaissance part avec elles. Ce chapitre traite de la documentation comme outil de communication et de prise de décision — pas comme bureaucratie.
Pourquoi documenter l’architecture#
La première réaction de beaucoup de développeurs face à la documentation est le scepticisme. La documentation est souvent perçue comme du travail supplémentaire qui devient obsolète dès la première modification du système. Ce scepticisme est légitime — il reflète une expérience réelle de documents Word de 200 pages qui ne correspondent plus à rien six mois après leur rédaction.
Mais ce scepticisme cible la mauvaise documentation, pas la documentation en général. La documentation architecturale utile répond à des questions précises que le code ne peut pas répondre :
Pourquoi ce système est-il structuré de cette façon et pas autrement ?
Quelles options ont été considérées et rejetées, et pourquoi ?
Quelles contraintes non techniques ont influencé les décisions techniques ?
Quelle est la vision d’ensemble pour un nouveau membre de l’équipe ?
Le code répond à « comment » — il décrit l’implémentation avec une précision absolue. La documentation architecturale répond à « pourquoi » — elle capture le raisonnement et le contexte.
La documentation comme outil de communication se manifeste de plusieurs façons concrètes :
Un diagramme de contexte C4 permet d’aligner l’équipe technique et les décideurs métier sur la même vision du système
Un ADR permet à un développeur qui rejoint l’équipe dans 18 mois de comprendre pourquoi le système utilise Kafka plutôt que RabbitMQ, sans avoir à fouiller l’historique Git
Un diagramme de séquence permet d’identifier des problèmes de cohérence ou de sécurité avant l’implémentation
La documentation vivante est le principe selon lequel la documentation doit être maintenue aussi proche que possible du code, idéalement générée ou validée automatiquement. Les tests d’acceptation BDD (Behavior-Driven Development) documentent le comportement attendu et sont exécutables. Les diagrammes C4 can be generated from code (Structurizr DSL). Les architecture fitness functions vérifient automatiquement que l’architecture respecte ses propres règles.
Documentation just enough
La documentation architecturale optimale est la plus légère possible qui répond aux questions que les parties prenantes poseront réellement. Un diagramme de contexte et cinq ADR bien rédigés ont plus de valeur qu’un document arc42 de 80 pages dont personne ne connaît l’existence. Commencer petit, documenter les décisions au fur et à mesure.
Architecture Decision Records — format MADR#
Les ADR (Architecture Decision Records) ont été introduits au chapitre 1. Ce chapitre approfondit le format MADR (Markdown Architecture Decision Records), développé par Olaf Zimmermann, qui est devenu le standard le plus utilisé dans les projets modernes.
Structure MADR#
Le format MADR enrichit le format de base de Nygard avec une section sur les options considérées et les critères de décision. Structure complète :
# ADR-NNN : [Titre court décrivant la décision]
## Statut
[Proposed | Accepted | Deprecated | Superseded by ADR-XXX]
## Contexte et problème
[Description du problème ou de la situation qui nécessite une décision.
Quelles forces sont en jeu ? Quelles contraintes ?]
## Critères de décision
* [Critère 1 — ex. latence < 10ms pour 99% des requêtes]
* [Critère 2 — ex. compétences disponibles dans l'équipe]
* [Critère 3 — ex. coût opérationnel < N€/mois]
## Options considérées
* Option A : [Titre]
* Option B : [Titre]
* Option C : [Titre]
## Résultat de la décision
Option choisie : "[Option X]", parce que [justification concise].
### Conséquences positives
* [Impact positif 1]
* [Impact positif 2]
### Conséquences négatives
* [Impact négatif 1 — trade-off accepté]
## Analyse des options
### Option A : [Titre]
[Description] — [Avantages] — [Inconvénients]
### Option B : [Titre]
...
Exemple 1 — Choix de base de données pour un service e-commerce#
# ADR-012 : PostgreSQL avec JSONB pour le service catalogue
## Statut
Accepted
## Contexte et problème
Le service catalogue doit stocker 500 000 références produits avec des
attributs hétérogènes (vêtements : 8 attributs, électronique : 25 attributs,
alimentation : 12 attributs). Les recherches combinent filtres fixes
(prix, catégorie) et filtres dynamiques (attributs spécifiques).
Volume projeté : 2000 requêtes/s en pic. Équipe de 4 développeurs,
tous expérimentés SQL, aucun avec MongoDB en production.
## Critères de décision
* Requêtes hybrides (fixe + dynamique) < 20ms au p95
* Transactions ACID pour la mise à jour simultanée prix/stock
* Pas de nouvelle compétence opérationnelle à acquérir
* Coût infrastructure < 500€/mois
## Options considérées
* PostgreSQL + JSONB
* MongoDB
* PostgreSQL + tables d'attributs EAV (Entity-Attribute-Value)
## Résultat de la décision
Option choisie : "PostgreSQL + JSONB", parce que les index GIN sur JSONB
permettent les requêtes hybrides performantes, les transactions ACID sont
garanties nativement, et l'équipe maîtrise PostgreSQL en production.
### Conséquences positives
* Transactions ACID sur prix et stock simultanément
* Index GIN : requêtes hybrides < 8ms au p95 (validé en charge test)
* Pas de nouvelle couche opérationnelle
### Conséquences négatives
* Requêtes JSONB moins lisibles que SQL pur — nécessite formation (2j)
* Évolution vers schéma 100% relationnel complexe si attributs se stabilisent
Exemple 2 — Choix communication synchrone vs asynchrone#
# ADR-018 : Kafka pour la communication entre le service commandes et le service stock
## Statut
Accepted
## Contexte et problème
Quand une commande est validée, le service stock doit être notifié pour
décrementer les quantités. Le service commandes appelle actuellement
le service stock de façon synchrone (HTTP). Problème : si le service stock
est indisponible (déploiement, incident), les commandes échouent.
Le taux d'erreur actuel est de 0,3% — inacceptable pour le SLO de 99,9%.
## Critères de décision
* Disponibilité des commandes indépendante de la disponibilité du stock
* Cohérence éventuelle acceptable (délai de mise à jour stock < 500ms)
* Pas de perte de messages (durabilité)
* Compétences Kafka disponibles dans l'équipe plateforme
## Options considérées
* Kafka (streaming de messages)
* RabbitMQ (broker de messages AMQP)
* Outbox pattern + polling (sans broker externe)
## Résultat de la décision
Option choisie : "Kafka", parce que la durabilité des messages,
le rejeu natif et la compétence existante dans l'équipe plateforme
l'emportent sur la simplicité de RabbitMQ.
Cycle de vie des ADR#
Les ADR ont un statut qui évolue dans le temps :
Proposed : décision soumise pour discussion, pas encore validée
Accepted : décision validée et en vigueur
Deprecated : décision abandonnée sans remplacement (la contrainte a disparu)
Superseded by ADR-NNN : décision remplacée par une nouvelle décision documentée dans ADR-NNN
Ne jamais modifier le contenu d’un ADR accepté. Si la décision change, créer un nouvel ADR et marquer l’ancien comme « superseded ». L’historique des décisions a de la valeur.
Le modèle C4 en détail#
Le modèle C4 (Simon Brown) fournit quatre niveaux de zoom pour documenter l’architecture. Le chapitre 1 en a posé les bases ; ce chapitre couvre la notation, les outils, et les pièges courants.
Notation officielle#
Personnes : dessinées comme des figurines ou des rectangles arrondis. Représentent les utilisateurs humains (internes ou externes).
Systèmes logiciels : rectangles. Le système que l’on documente est mis en évidence (couleur différente). Les systèmes externes sont plus discrets.
Conteneurs : rectangles avec le nom et la technologie en sous-titre. Exemples : « API REST (Spring Boot) », « Base de données (PostgreSQL) », « File de messages (Kafka) ».
Composants : rectangles au sein d’un conteneur. Représentent des regroupements logiques de code — contrôleurs, services, repositories.
Relations : flèches avec étiquette décrivant la nature de l’interaction et le protocole si pertinent. « Lit/écrit via JDBC », « Publie des événements sur », « Appelle via REST/HTTPS ».
Règle des étiquettes
Toute flèche dans un diagramme C4 doit avoir une étiquette. « Utilise » n’est pas une étiquette — c’est une non-information. « Lit les commandes via JDBC », « Publie un événement OrderPlaced sur », « Appelle l’API de paiement via REST/HTTPS » sont des étiquettes informatives.
Outils#
Structurizr (l’outil officiel de Simon Brown) permet de définir l’architecture en code (DSL ou Java) et de générer les quatre niveaux de diagrammes de façon cohérente. La cohérence est l’avantage principal : un composant défini une fois apparaît correctement dans tous les niveaux.
PlantUML avec l’extension C4 est la solution la plus répandue dans les projets open-source. Verbeux mais intégrable dans la documentation as code.
Mermaid supporte un sous-ensemble du C4 (niveaux contexte et conteneur) directement dans Markdown. Idéal pour les ADR et les wikis de projet.
draw.io / Excalidraw pour les diagrammes ad hoc lors de discussions d’équipe. Pas versionnable automatiquement, mais suffisant pour les sessions de travail.
Pièges courants#
Trop de détails au mauvais niveau : un diagramme de contexte avec des tables de base de données n’est pas un diagramme de contexte, c’est un diagramme de schéma.
Flèches sans étiquette : rend le diagramme ambigu.
Diagrammes C4 pour tout : le niveau Code (niveau 4) est rarement utile en documentation statique — les IDE le génèrent à la demande.
Diagrammes non maintenus : un diagramme qui ne correspond plus au système réel est pire qu’une absence de diagramme.
arc42 — template de documentation architecturale#
arc42 est un template de documentation architecturale open-source, développé par Gernot Starke et Peter Hruschka. Il organise la documentation en 12 sections couvrant tous les aspects d’une architecture logicielle.
Les 12 sections arc42#
Introduction et objectifs : exigences fonctionnelles clés, parties prenantes, objectifs de qualité
Contraintes : contraintes techniques et organisationnelles non négociables
Contexte et périmètre : contexte métier (interactions avec partenaires/utilisateurs) et contexte technique (interfaces avec systèmes externes)
Stratégie de solution : décisions architecturales fondamentales, approches pour satisfaire les objectifs de qualité
Vue des blocs de construction : décomposition statique du système en boîtes blanches et noires
Vue d’exécution : comportement dynamique, scénarios importants, interactions entre composants
Vue de déploiement : infrastructure technique, mapping composants/nœuds
Concepts transversaux : patterns récurrents, règles qui s’appliquent à l’ensemble du système (gestion des erreurs, logging, sécurité)
Décisions architecturales : ici vont les ADR
Exigences de qualité : arbre de qualité, scénarios de qualité (ATAM)
Risques et dette technique : risques identifiés, dette technique documentée
Glossaire : termes métier et techniques importants
Quand utiliser arc42 ?#
arc42 est adapté aux systèmes de grande envergure, aux projets où de nombreuses parties prenantes ont besoin d’une vision commune, et aux contextes réglementés (finance, santé, défense) qui exigent une documentation formelle.
Pour les projets de taille intermédiaire, on peut utiliser arc42 partiellement — les sections 1, 3, 5, 7 et 9 (ADR) couvrent l’essentiel sans surcharge.
arc42 et agilité
arc42 est compatible avec les méthodes agiles. Il n’impose pas une rédaction exhaustive avant le développement. Les sections peuvent être complétées de façon incrémentale, à mesure que les décisions sont prises. La section 9 (ADR) s’enrichit naturellement au rythme des itérations.
Diagrammes de séquence#
Les diagrammes de séquence UML représentent les interactions entre composants dans le temps. Ils sont particulièrement utiles pour documenter des flux complexes impliquant plusieurs services ou acteurs.
Notation simplifiée#
Lignes de vie (lifelines) : colonnes verticales représentant les acteurs/composants
Messages : flèches horizontales avec étiquette (appel synchrone → flèche pleine, réponse → flèche pointillée)
Activation : rectangle sur une ligne de vie indiquant que le composant est actif
Blocs :
loop,alt,opt,parpour les structures conditionnelles et parallèles
Cas d’usage typiques#
Appel API avec authentification : illustre le flux d’un token JWT depuis l’identityprovider jusqu’au service métier, les vérifications de validité, les codes de retour.
Saga de commande e-commerce : dans un contexte microservices, la saga orchestre des transactions distribuées sans verrou global. Le diagramme de séquence montre les messages échangés entre les services (commandes, paiement, stock, livraison) et les compensations en cas d’échec.
Pattern Outbox : pour garantir la cohérence entre une écriture en base de données et la publication d’un événement, le diagramme de séquence illustre l’écriture dans la table outbox, le polling par le relai, et la publication sur Kafka.
Diagrammes d’état#
Les diagrammes d’état (statecharts) modélisent le cycle de vie d’une entité — les états qu’elle peut occuper et les transitions entre ces états. Ils sont indispensables pour les systèmes réactifs et les entités avec un cycle de vie complexe.
Notation#
État : rectangle arrondi avec le nom de l’état
Transition : flèche entre états, étiquetée
événement [garde] / actionÉtat initial : cercle plein
État final : cercle plein dans un anneau
États composites : états qui contiennent d’autres états (gestion des sous-états)
Exemples#
Commande e-commerce : les états typiques sont Panier, En attente de paiement, Paiement confirmé, En préparation, Expédiée, Livrée, Annulée, Remboursée. Chaque transition est déclenchée par un événement métier (paiement reçu, colis scanné, client annule) et peut déclencher une action (envoyer email, mettre à jour stock).
Connexion utilisateur : Déconnecté → Authentification en cours → Connecté → Session expirée → Déconnecté. Avec des gardes : le timeout de session n’est actif que si l’option « remember me » est désactivée.
Disjoncteur (Circuit Breaker) : Fermé (fonctionnement normal) → Ouvert (service down, rejeter les requêtes) → Semi-ouvert (test de récupération) → Fermé ou Ouvert selon le résultat du test.
Documentation vivante — tests, fitness functions et ArchUnit#
La documentation vivante est le principe selon lequel la documentation la plus fiable est celle qui est générée ou validée automatiquement à partir du code réel.
Tests comme documentation#
Les tests d’acceptation BDD (Cucumber, Behave) formalisent le comportement attendu en langage naturel. Un scénario Gherkin est à la fois de la documentation lisible par un non-technicien et un test exécutable.
Scénario: Passage d'une commande avec stock suffisant
Étant donné un produit "Laptop Pro" avec un stock de 5 unités
Et un utilisateur authentifié "alice@example.com"
Quand alice passe une commande pour 2 unités
Alors le stock du "Laptop Pro" est de 3 unités
Et alice reçoit un email de confirmation
Et le statut de la commande est "En attente de paiement"
Ce scénario documente le comportement métier attendu avec une précision que ne pourrait atteindre aucune prose.
Architecture Fitness Functions#
Les fitness functions architecturales (Building Evolutionary Architectures, Ford, Parsons & Kua) sont des tests automatisés qui vérifient que l’architecture respecte ses propres règles structurelles.
Exemples de règles vérifiables :
Aucune dépendance cyclique entre packages
Les composants du domaine ne dépendent pas de l’infrastructure
Toutes les API exposées sont documentées (OpenAPI)
Aucune requête SQL dans la couche contrôleur
Le temps de démarrage reste inférieur à 10 secondes
Ces règles s’érodent progressivement sans contrôle automatique. Une dépendance cyclique commence par un cas, puis s’en ajoute un second « parce que le premier l’a bien fait », jusqu’à ce que le module entier soit impossible à tester unitairement.
ArchUnit#
ArchUnit est une bibliothèque Java/Kotlin qui permet d’écrire des tests JUnit vérifiant des contraintes architecturales sur le bytecode.
// Règle : les classes du package "domain" ne dépendent pas de "infrastructure"
@Test
void domain_should_not_depend_on_infrastructure() {
noClasses().that().resideInAPackage("..domain..")
.should().dependOnClassesThat()
.resideInAPackage("..infrastructure..")
.check(importedClasses);
}
Des équivalents existent pour d’autres langages : PyTest-Archon pour Python, Dependency-Cruiser pour JavaScript/TypeScript.
La documentation vivante en pratique
Commencer par les fitness functions les plus importantes : pas de dépendances cycliques, séparation des couches. Ces deux règles, appliquées automatiquement en CI, préviennent la majorité de la dette architecturale accidentelle. Ajouter les règles métier spécifiques au fur et à mesure.
Visualisations#
Diagramme de séquence — authentification JWT#
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import numpy as np
import seaborn as sns
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
fig, ax = plt.subplots(figsize=(13, 9))
ax.set_xlim(0, 13)
ax.set_ylim(0, 9.5)
ax.axis('off')
ax.set_facecolor('#fafafa')
fig.patch.set_facecolor('#fafafa')
# Acteurs
actors = [
(1.3, "Client\n(Browser)"),
(4.5, "API Gateway"),
(7.5, "Auth Service"),
(10.8, "Order Service"),
]
actor_x = [a[0] for a in actors]
actor_labels = [a[1] for a in actors]
colors_actors = ['#1168BD', '#2d6a4f', '#e07b39', '#6c3483']
# En-têtes des acteurs
for x, label, color in zip(actor_x, actor_labels, colors_actors):
bbox = mpatches.FancyBboxPatch((x - 0.7, 8.9), 1.4, 0.5,
boxstyle="round,pad=0.05",
facecolor=color, edgecolor='white',
linewidth=1, zorder=3)
ax.add_patch(bbox)
ax.text(x, 9.15, label, ha='center', va='center',
fontsize=8.5, fontweight='bold', color='white', zorder=4)
# Lignes de vie
y_top = 8.88
y_bottom = 0.3
for x, color in zip(actor_x, colors_actors):
ax.plot([x, x], [y_top, y_bottom], color=color, linewidth=1.2,
linestyle='--', alpha=0.5, zorder=1)
# Messages (y décroissant = temps qui avance)
messages = [
# (y, x_from, x_to, label, sync=True, response=False)
(8.3, 1.3, 4.5, "POST /login (email, password)", True, False),
(7.6, 4.5, 7.5, "validateCredentials(email, password)", True, False),
(6.9, 7.5, 4.5, "JWT token + refresh token", True, True),
(6.2, 4.5, 1.3, "200 OK { access_token, expires_in }", True, True),
(5.4, 1.3, 4.5, "GET /orders [Authorization: Bearer <token>]", True, False),
(4.7, 4.5, 7.5, "verifyToken(token)", True, False),
(4.0, 7.5, 4.5, "{ userId, roles } — token valide", True, True),
(3.3, 4.5, 10.8, "getOrders(userId)", True, False),
(2.6, 10.8, 4.5, "[ Order1, Order2, ... ]", True, True),
(1.9, 4.5, 1.3, "200 OK { orders: [...] }", True, True),
]
for y, x1, x2, label, sync, response in messages:
going_right = x2 > x1
color = '#333333' if not response else '#555555'
linestyle = '-' if not response else '--'
linewidth = 1.5 if not response else 1.2
dx = 0.08
ax.annotate("", xy=(x2 + (dx if not going_right else -dx), y),
xytext=(x1 + (dx if going_right else -dx), y),
arrowprops=dict(arrowstyle="-|>", color=color,
lw=linewidth, linestyle=linestyle))
mid_x = (x1 + x2) / 2
offset_y = 0.12
ax.text(mid_x, y + offset_y, label, ha='center', va='bottom',
fontsize=7.8, color=color,
style='italic' if response else 'normal')
# Activation rectangles (optionnels, cosmétiques)
activation_boxes = [
(4.5, 8.3, 1.9, '#2d6a4f'), # API Gateway actif tout du long
(7.5, 7.6, 4.0, '#e07b39'), # Auth actif
(10.8, 3.3, 2.6, '#6c3483'), # Order actif
]
for x, y_start, y_end, color in activation_boxes:
rect = mpatches.FancyBboxPatch((x - 0.12, y_end), 0.24, y_start - y_end,
boxstyle="square,pad=0",
facecolor=color, edgecolor='white',
linewidth=0.8, alpha=0.35, zorder=2)
ax.add_patch(rect)
ax.set_title("Diagramme de séquence — Authentification JWT et accès API",
fontsize=12, fontweight='bold', pad=10)
# Légende
ax.text(0.2, 0.4, "→ appel synchrone - - → réponse",
fontsize=8, color='#555555', style='italic')
plt.savefig("sequence_auth.png", dpi=120, bbox_inches='tight')
plt.show()
Machine à états — commande e-commerce#
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import networkx as nx
import seaborn as sns
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
fig, ax = plt.subplots(figsize=(13, 8))
ax.set_facecolor('#fafafa')
fig.patch.set_facecolor('#fafafa')
ax.axis('off')
# Noeuds (états) et positions
states = {
"Panier": (1.5, 6.5),
"En attente\npaiement": (4.5, 6.5),
"Paiement\nconfirmé": (7.5, 6.5),
"En\npréparation": (7.5, 4.0),
"Expédiée": (7.5, 1.8),
"Livrée": (4.5, 0.5),
"Annulée": (4.5, 4.0),
"Remboursée": (1.5, 4.0),
}
state_colors = {
"Panier": "#1168BD",
"En attente\npaiement": "#e07b39",
"Paiement\nconfirmé": "#2d6a4f",
"En\npréparation": "#2d6a4f",
"Expédiée": "#1168BD",
"Livrée": "#27ae60",
"Annulée": "#c0392b",
"Remboursée": "#8e44ad",
}
# Dessiner les états
for state, (x, y) in states.items():
color = state_colors[state]
is_terminal = state in ["Livrée", "Annulée", "Remboursée"]
box_style = "round,pad=0.15"
bbox = mpatches.FancyBboxPatch((x - 0.7, y - 0.38), 1.4, 0.76,
boxstyle=box_style,
facecolor=color, edgecolor='white',
linewidth=2 if is_terminal else 1,
alpha=0.9, zorder=3)
ax.add_patch(bbox)
ax.text(x, y, state, ha='center', va='center',
fontsize=8.5, fontweight='bold', color='white', zorder=4)
if is_terminal:
ring = plt.Circle((x, y), 0.52, fill=False, edgecolor=color,
linewidth=2.5, zorder=2, alpha=0.6)
ax.add_patch(ring)
# État initial
ax.annotate("", xy=(1.5, 6.88), xytext=(1.5, 7.5),
arrowprops=dict(arrowstyle="-|>", color='#333333', lw=1.8))
ax.plot(1.5, 7.62, 'o', color='#333333', markersize=10, zorder=5)
# Transitions
transitions = [
# (from_state, to_state, label, color, bend)
("Panier", "En attente\npaiement", "Commander\n(valider panier)", "#555555", 0),
("En attente\npaiement", "Paiement\nconfirmé", "Paiement reçu\n[stripe webhook]", "#2d6a4f", 0),
("En attente\npaiement", "Annulée", "Timeout 30min\nou annulation client", "#c0392b", 0),
("Paiement\nconfirmé", "En\npréparation", "Affectation\nentrepôt", "#555555", 0),
("En\npréparation", "Expédiée", "Colis scanné\n(départ entrepôt)", "#1168BD", 0),
("Expédiée", "Livrée", "Livraison\nconfirmée", "#27ae60", 0),
("Expédiée", "Annulée", "Retour client\n(délai dépassé)", "#c0392b", 0.3),
("Annulée", "Remboursée", "Paiement\ndéjà effectué", "#8e44ad", 0),
("Panier", "Annulée", "Abandon\npanier (7j)", "#c0392b", -0.4),
]
for from_s, to_s, label, color, bend in transitions:
x1, y1 = states[from_s]
x2, y2 = states[to_s]
rad = f"arc3,rad={bend}" if bend != 0 else "arc3,rad=0"
ax.annotate("", xy=(x2, y2 + 0.38 if y2 > y1 else y2 - 0.38 if y2 < y1 else y2),
xytext=(x1, y1 - 0.38 if y2 < y1 else y1 + 0.38 if y2 > y1 else x1),
arrowprops=dict(arrowstyle="-|>", color=color, lw=1.4,
connectionstyle=rad))
mid_x = (x1 + x2) / 2 + (bend * 1.2 if bend != 0 else 0)
mid_y = (y1 + y2) / 2
ax.text(mid_x + 0.1, mid_y, label, ha='left', va='center',
fontsize=7.5, color=color, style='italic',
bbox=dict(boxstyle='round,pad=0.2', facecolor='white',
edgecolor='none', alpha=0.8))
ax.set_xlim(0, 10)
ax.set_ylim(-0.2, 8.2)
ax.set_title("Machine à états — Cycle de vie d'une commande e-commerce",
fontsize=12, fontweight='bold', pad=10)
# Légende statuts terminaux
terminal_patch = mpatches.Patch(facecolor='white', edgecolor='#333333',
linewidth=2, label='État terminal (double bordure)')
ax.legend(handles=[terminal_patch], loc='lower right', fontsize=8.5)
plt.savefig("state_machine_order.png", dpi=120, bbox_inches='tight')
plt.show()
Template ADR avec statuts colorés#
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)
adrs = [
{
"num": "ADR-001",
"titre": "Architecture microservices pour le back-end",
"statut": "accepted",
"contexte": "Équipe de 25 dev, 4 domaines métier distincts\n(catalogue, commandes, stock, livraison)",
"decision": "Microservices par domaine métier (DDD bounded contexts)\navec communication asynchrone via Kafka",
"consequences": "+ Autonomie des équipes\n+ Scalabilité indépendante\n− Complexité opérationnelle (K8s)",
},
{
"num": "ADR-007",
"titre": "PostgreSQL + JSONB pour le service catalogue",
"statut": "accepted",
"contexte": "Attributs produits hétérogènes selon catégorie\n(vêtements, électronique, alimentation)",
"decision": "PostgreSQL avec JSONB pour les attributs variables\nIndex GIN pour les requêtes hybrides",
"consequences": "+ Transactions ACID\n+ Équipe formée SQL\n− Requêtes JSONB moins lisibles",
},
{
"num": "ADR-012",
"titre": "REST synchrone entre gateway et services",
"statut": "superseded",
"contexte": "Communication initiale entre API Gateway\net services métier par HTTP/REST",
"decision": "REST/JSON synchrone avec timeouts courts\net circuit breakers (Resilience4j)",
"consequences": "Superseded par ADR-018 (Kafka async)\nà cause des 0.3% d'erreurs de timeout",
},
{
"num": "ADR-018",
"titre": "Kafka pour commandes → stock",
"statut": "accepted",
"contexte": "Timeout du service stock causait 0.3% d'échecs\ncommande — SLO de 99.9% non respecté",
"decision": "Événements asynchrones via Kafka (OrderPlaced)\nCohérence éventuelle < 500ms acceptable",
"consequences": "+ Disponibilité commandes découplée de stock\n+ Rejeu natif\n− Cohérence éventuelle à gérer",
},
{
"num": "ADR-023",
"titre": "GraphQL pour l'API mobile",
"statut": "proposed",
"contexte": "L'app mobile nécessite des agrégations complexes\ncausant du over-fetching avec REST",
"decision": "GraphQL federation devant les microservices\nPOC en cours avec Apollo Server",
"consequences": "En cours d'évaluation — décision prévue S1 2026",
},
]
statut_colors = {
"accepted": {"bg": "#d4edda", "border": "#28a745", "label": "#155724", "text": "ACCEPTED"},
"superseded": {"bg": "#fff3cd", "border": "#ffc107", "label": "#856404", "text": "SUPERSEDED"},
"deprecated": {"bg": "#f8d7da", "border": "#dc3545", "label": "#721c24", "text": "DEPRECATED"},
"proposed": {"bg": "#cce5ff", "border": "#0066cc", "label": "#004085", "text": "PROPOSED"},
}
n = len(adrs)
fig_height = n * 1.65 + 0.8
fig, ax = plt.subplots(figsize=(14, fig_height))
ax.axis('off')
ax.set_xlim(0, 14)
ax.set_ylim(0, fig_height)
ax.set_facecolor('#f8f9fa')
fig.patch.set_facecolor('#f8f9fa')
ax.text(7, fig_height - 0.3, "Architecture Decision Records — Journal de bord",
ha='center', va='top', fontsize=13, fontweight='bold', color='#222222')
row_h = 1.5
col_widths = [1.2, 3.0, 3.5, 3.8]
col_x = [0.2, 1.5, 4.7, 8.4]
headers = ["Statut", "Décision", "Contexte", "Conséquences"]
# En-têtes colonnes
header_y = fig_height - 0.75
for header, x, w in zip(headers, col_x, col_widths):
rect = mpatches.FancyBboxPatch((x, header_y - 0.2), w - 0.1, 0.38,
boxstyle="round,pad=0.04",
facecolor='#1168BD', edgecolor='none',
transform=ax.transData)
ax.add_patch(rect)
ax.text(x + (w - 0.1) / 2, header_y,
header, ha='center', va='center',
fontsize=9, fontweight='bold', color='white')
for i, adr in enumerate(adrs):
y_row = fig_height - 1.2 - i * row_h
sc = statut_colors[adr["statut"]]
# Fond de la ligne
row_bg = '#ffffff' if i % 2 == 0 else '#f0f4ff'
bg_rect = mpatches.FancyBboxPatch((0.15, y_row - 0.58), 13.7, row_h - 0.12,
boxstyle="round,pad=0.05",
facecolor=row_bg, edgecolor='#dee2e6',
linewidth=0.5)
ax.add_patch(bg_rect)
# Badge statut
badge = mpatches.FancyBboxPatch((col_x[0], y_row - 0.25), col_widths[0] - 0.15, 0.5,
boxstyle="round,pad=0.05",
facecolor=sc["bg"], edgecolor=sc["border"],
linewidth=1.5)
ax.add_patch(badge)
ax.text(col_x[0] + (col_widths[0] - 0.15) / 2, y_row + 0.02,
sc["text"], ha='center', va='center',
fontsize=7.5, fontweight='bold', color=sc["label"])
# Numéro + titre décision
ax.text(col_x[1], y_row + 0.28, adr["num"],
ha='left', va='center', fontsize=8, color='#1168BD', fontweight='bold')
ax.text(col_x[1], y_row + 0.05, adr["titre"],
ha='left', va='center', fontsize=8.5, fontweight='bold', color='#222222')
ax.text(col_x[1], y_row - 0.25, adr["decision"],
ha='left', va='top', fontsize=7.8, color='#444444', linespacing=1.4)
# Contexte
ax.text(col_x[2], y_row + 0.15, adr["contexte"],
ha='left', va='center', fontsize=7.8, color='#444444', linespacing=1.4)
# Conséquences
ax.text(col_x[3], y_row + 0.15, adr["consequences"],
ha='left', va='center', fontsize=7.8, color='#444444', linespacing=1.4)
plt.savefig("adr_tableau_complet.png", dpi=120, bbox_inches='tight')
plt.show()
Visualisation C4 — Niveau Conteneur#
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)
fig, ax = plt.subplots(figsize=(14, 9))
ax.set_xlim(0, 14)
ax.set_ylim(0, 9)
ax.axis('off')
ax.set_facecolor('#f0f4ff')
fig.patch.set_facecolor('#f0f4ff')
# Frontière du système
system_border = mpatches.FancyBboxPatch((1.5, 0.8), 11, 7.4,
boxstyle="round,pad=0.15",
facecolor='#e8f0fd', edgecolor='#1168BD',
linewidth=2, linestyle='--', alpha=0.5)
ax.add_patch(system_border)
ax.text(7.0, 8.1, "Boutique en ligne [Système logiciel]",
ha='center', va='center', fontsize=10, fontweight='bold',
color='#1168BD', style='italic')
def draw_container(ax, x, y, w, h, name, tech, color="#1168BD"):
bbox = mpatches.FancyBboxPatch((x - w/2, y - h/2), w, h,
boxstyle="round,pad=0.08",
facecolor=color, edgecolor='white',
linewidth=1.5, zorder=3)
ax.add_patch(bbox)
ax.text(x, y + 0.18, name, ha='center', va='center',
fontsize=9, fontweight='bold', color='white', zorder=4)
ax.text(x, y - 0.22, f"[{tech}]", ha='center', va='center',
fontsize=7.8, color='white', alpha=0.85, style='italic', zorder=4)
def draw_external(ax, x, y, w, h, name, tech):
bbox = mpatches.FancyBboxPatch((x - w/2, y - h/2), w, h,
boxstyle="round,pad=0.08",
facecolor='#6B7280', edgecolor='white',
linewidth=1, zorder=3)
ax.add_patch(bbox)
ax.text(x, y + 0.18, name, ha='center', va='center',
fontsize=9, fontweight='bold', color='white', zorder=4)
ax.text(x, y - 0.22, f"[{tech}]", ha='center', va='center',
fontsize=7.8, color='white', alpha=0.85, style='italic', zorder=4)
def draw_person_small(ax, x, y, name, role):
circle = plt.Circle((x, y + 0.3), 0.22, color='#1168BD', zorder=3)
ax.add_patch(circle)
ax.plot([x, x], [y + 0.08, y - 0.25], color='#1168BD', linewidth=1.5, zorder=3)
ax.plot([x - 0.25, x + 0.25], [y - 0.05, y - 0.05], color='#1168BD', linewidth=1.5, zorder=3)
ax.text(x, y - 0.55, name, ha='center', va='top', fontsize=8.5, fontweight='bold', color='#333333')
ax.text(x, y - 0.82, role, ha='center', va='top', fontsize=7.5, color='#666666', style='italic')
def draw_rel(ax, x1, y1, x2, y2, label, color='#555555', bend=0):
rad = f"arc3,rad={bend}" if bend != 0 else "arc3,rad=0"
ax.annotate("", xy=(x2, y2), xytext=(x1, y1),
arrowprops=dict(arrowstyle="-|>", color=color, lw=1.3,
connectionstyle=rad))
mx, my = (x1 + x2) / 2 + bend * 0.8, (y1 + y2) / 2
ax.text(mx, my + 0.12, label, ha='center', va='bottom',
fontsize=7.5, color=color, style='italic',
bbox=dict(boxstyle='round,pad=0.15', facecolor='white',
edgecolor='none', alpha=0.85))
# Personnes
draw_person_small(ax, 0.7, 6.5, "Client", "[Personne]")
draw_person_small(ax, 0.7, 2.5, "Admin", "[Personne]")
# Conteneurs internes
draw_container(ax, 4.0, 7.2, 2.6, 0.9, "Web App", "React / Next.js", "#1168BD")
draw_container(ax, 4.0, 5.5, 2.6, 0.9, "API Gateway", "Kong / Node.js", "#1168BD")
draw_container(ax, 4.0, 3.5, 2.6, 0.9, "Admin SPA", "React / Vite", "#1168BD")
draw_container(ax, 7.5, 7.2, 2.4, 0.9, "Service Catalogue", "Python / FastAPI", "#2d6a4f")
draw_container(ax, 7.5, 5.5, 2.4, 0.9, "Service Commandes", "Java / Spring Boot", "#2d6a4f")
draw_container(ax, 7.5, 3.8, 2.4, 0.9, "Service Stock", "Go / Gin", "#2d6a4f")
draw_container(ax, 7.5, 2.1, 2.4, 0.9, "Service Notifs", "Node.js", "#2d6a4f")
draw_container(ax, 10.8, 6.0, 2.2, 0.9, "PostgreSQL", "Base de données", "#6c3483")
draw_container(ax, 10.8, 4.0, 2.2, 0.9, "Kafka", "Bus de messages", "#e07b39")
draw_container(ax, 10.8, 2.2, 2.2, 0.9, "Redis", "Cache / Sessions", "#c0392b")
# Systèmes externes
draw_external(ax, 4.0, 1.4, 2.4, 0.7, "Stripe", "Paiement [Externe]")
draw_external(ax, 7.5, 1.0, 2.4, 0.7, "Mailchimp", "Emails [Externe]")
# Relations
draw_rel(ax, 1.25, 6.5, 2.7, 7.2, "HTTPS", '#1168BD')
draw_rel(ax, 1.25, 2.7, 2.7, 3.5, "HTTPS", '#1168BD')
draw_rel(ax, 4.0, 6.77, 4.0, 5.95, "HTTPS", '#1168BD')
draw_rel(ax, 5.3, 7.2, 6.3, 7.2, "REST", '#2d6a4f')
draw_rel(ax, 5.3, 5.5, 6.3, 5.5, "REST", '#2d6a4f')
draw_rel(ax, 5.3, 3.5, 6.3, 3.8, "REST", '#2d6a4f')
draw_rel(ax, 8.7, 7.2, 9.7, 6.2, "JDBC", '#6c3483')
draw_rel(ax, 8.7, 5.5, 9.7, 5.8, "JDBC", '#6c3483')
draw_rel(ax, 8.7, 5.2, 9.7, 4.3, "Publie\névénements", '#e07b39')
draw_rel(ax, 9.7, 3.8, 8.7, 2.4, "Consomme\névénements", '#e07b39', bend=-0.2)
draw_rel(ax, 8.7, 3.5, 9.7, 2.5, "GET/SET", '#c0392b')
draw_rel(ax, 7.5, 1.65, 5.5, 1.4, "webhook\npaiement", '#6B7280', bend=0.2)
draw_rel(ax, 7.5, 1.55, 7.5, 1.35, "SMTP/API", '#6B7280')
ax.set_title("Diagramme C4 — Niveau Conteneur : Boutique en ligne",
fontsize=13, fontweight='bold', pad=10)
# Légende
patches_legend = [
mpatches.Patch(color='#1168BD', label='Frontend / Gateway'),
mpatches.Patch(color='#2d6a4f', label='Microservices métier'),
mpatches.Patch(color='#6c3483', label='Bases de données'),
mpatches.Patch(color='#e07b39', label='Bus de messages'),
mpatches.Patch(color='#6B7280', label='Systèmes externes'),
]
ax.legend(handles=patches_legend, loc='lower left',
fontsize=8, title="Légende", title_fontsize=8.5)
plt.savefig("c4_container.png", dpi=120, bbox_inches='tight')
plt.show()
Résumé#
La documentation architecturale n’est pas un artefact bureaucratique. C’est l’outil qui permet à une équipe de prendre des décisions cohérentes sur la durée, de communiquer avec des parties prenantes non techniques, et de préserver la connaissance au-delà des rotations d’équipe.
Pourquoi documenter : le code répond à « comment », la documentation répond à « pourquoi ». Les ADR capturent le raisonnement qui a conduit à une décision — information introuvable dans le code ou même dans l’historique Git.
Format MADR : le format le plus complet et le plus adopté. Contexte, critères de décision, options considérées, résultat, conséquences positives et négatives. Versionnés avec le code, les ADR constituent la mémoire collective de l’architecture. Ne jamais modifier un ADR accepté — le superseder.
Modèle C4 : quatre niveaux progressifs (Contexte, Conteneur, Composant, Code) adaptés à des audiences différentes. La notation est volontairement simple. Toute flèche doit avoir une étiquette informative. Structurizr, PlantUML, Mermaid sont les outils de référence.
arc42 : template en 12 sections pour les systèmes de grande envergure. Utilisable partiellement — les sections 1, 3, 5, 7 et 9 couvrent l’essentiel pour les projets de taille intermédiaire.
Diagrammes de séquence : indispensables pour les flux complexes multi-services (authentification, saga, pattern Outbox). Permettent d’identifier des problèmes de sécurité ou de cohérence avant l’implémentation.
Machines à états : modélisent le cycle de vie des entités complexes (commandes, connexions, circuit breakers). Rendent explicites les transitions et les gardes qui sont souvent implicites dans le code.
Documentation vivante : tests BDD, fitness functions architecturales (ArchUnit), génération depuis le code (Structurizr DSL). La documentation la plus fiable est celle qui est automatiquement vérifiée par le pipeline CI.
Points clés à retenir
La documentation répond à « pourquoi » — le code répond à « comment »
Les ADR doivent être versionnés avec le code, jamais modifiés, uniquement superseded
C4 : commencer avec le niveau Contexte (10 minutes sur un tableau blanc) et affiner selon le besoin
arc42 peut être utilisé partiellement — inutile de remplir les 12 sections si 5 suffisent
Les diagrammes d’état évitent les bugs de gestion de cycle de vie en rendant explicite ce qui est sinon implicite dans des conditions éparpillées
Les fitness functions architecturales (ArchUnit) transforment les règles architecturales en tests automatiques — la seule documentation qui ne peut pas être obsolète