Anti-patterns en architecture logicielle
Erreurs courantes en architecture logicielle distribuée : comment les identifier, leur impact réel et les stratégies pour les corriger.
Que sont les anti-patterns
Un anti-pattern est une solution récurrente qui paraît raisonnable au premier abord mais qui engendre des problèmes importants à moyen et long terme. Contrairement à une simple erreur, les anti-patterns sont des pièges courants dans lesquels tombent des équipes expérimentées, souvent à cause de la pression du temps, d’un manque de contexte ou de décisions qui étaient correctes à un moment donné mais qui ont cessé de l’être.
Reconnaître ces anti-patterns à temps vous permet d’éviter une dette technique coûteuse et de prendre des décisions architecturales mieux éclairées.
Pourquoi les anti-patterns apparaissent
Les anti-patterns n’apparaissent pas par incompétence. Ils surgissent pour des raisons compréhensibles :
| Cause | Exemple |
|---|---|
| Pression du temps | « Ajoutons cette fonctionnalité au service existant, il n’y a pas le temps d’en créer un nouveau » |
| Manque de contexte | « Tous les services peuvent lire dans cette base de données, c’est plus simple » |
| Décisions qui vieillissent | « Quand nous étions 3 développeurs, ça fonctionnait bien ; maintenant nous sommes 30 » |
| Optimisation prématurée | « Séparons tout en microservices dès le premier jour » |
| Inertie organisationnelle | « On a toujours fait comme ça » |
1. Monolithe distribué (Distributed Monolith)
Problème
Il se produit lorsqu’un système est divisé en plusieurs services qui dépendent si fortement les uns des autres qu’ils ne peuvent être déployés, mis à l’échelle ni évoluer de façon indépendante. En pratique, vous avez la complexité opérationnelle des microservices sans aucun de leurs avantages.
┌──────────┐ ┌──────────┐ ┌──────────┐
│Servicio A│────►│Servicio B│────►│Servicio C│
│ │◄────│ │◄────│ │
└──────────┘ └──────────┘ └──────────┘
│ │ │
└────────────────┴────────────────┘
Despliegue conjunto obligatorio
Modelos de datos compartidos
Llamadas síncronas en cadena
Les signes les plus évidents sont :
- Vous devez déployer plusieurs services en même temps pour qu’un changement fonctionne
- Un changement dans le contrat d’un service casse les autres
- Les services partagent des bibliothèques internes contenant de la logique métier
- Les tests d’intégration nécessitent de démarrer tout le système
Impact
- Déploiements fragiles : Un changement dans un service oblige à coordonner le déploiement des autres, éliminant l’agilité qui avait motivé la séparation
- Défaillances en cascade : La dépendance synchrone entre services fait que la panne de l’un entraîne les autres
- Complexité sans bénéfice : L’équipe paie le coût opérationnel de plusieurs services sans gagner d’indépendance réelle
- Vitesse de développement réduite : Les équipes ne peuvent pas travailler en parallèle car les changements se bloquent mutuellement
Comment le détecter
- Posez la question : puis-je déployer ce service sans toucher à aucun autre ? Si la réponse est « non » fréquemment, vous avez un monolithe distribué
- Vérifiez si les services partagent des schémas de base de données ou des bibliothèques contenant de la logique de domaine
- Observez si les mises en production exigent une coordination entre plusieurs équipes pour un seul changement fonctionnel
- Mesurez le coupling score : comptez combien de services sont déployés ensemble à chaque mise en production
Correction
| Stratégie | Description |
|---|---|
| Définir des contrats clairs | Utiliser des API versionnées avec des contrats explicites. Chaque service expose une interface stable et évolue en interne sans casser ses consommateurs |
| Communication asynchrone | Remplacer les appels synchrones en chaîne par des événements. Publier un événement au lieu de faire un appel direct |
| Bounded Contexts (DDD) | Appliquer le DDD pour identifier les limites réelles du domaine. Chaque service doit être propriétaire de son contexte |
| Évaluer une re-consolidation | Si les services sont si couplés qu’ils ne peuvent être séparés, envisagez de les réunir à nouveau. Un monolithe bien conçu vaut mieux qu’un monolithe distribué |
| Anti-Corruption Layer | Utiliser un ACL entre les services pour traduire les modèles et réduire le couplage direct |
2. Base de données partagée (Shared Database)
Problème
Plusieurs services lisent et écrivent directement dans la même base de données, en accédant aux mêmes tables. Cela crée un couplage invisible : les services ne dépendent pas d’API explicites mais de la structure interne des tables.
┌──────────┐ ┌──────────┐ ┌──────────┐
│Servicio A│ │Servicio B│ │Servicio C│
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
▼ ▼ ▼
┌──────────────────────────────────────┐
│ Base de Datos Compartida │
│ │
│ ┌─────────┐ ┌─────────┐ ┌────────┐ │
│ │ Tabla X │ │ Tabla Y │ │ Tabla Z│ │
│ └─────────┘ └─────────┘ └────────┘ │
│ │
│ Todos leen y escriben en todas │
└──────────────────────────────────────┘
Les signes les plus évidents sont :
- Plusieurs services font des requêtes directes sur les mêmes tables
- Un changement de schéma oblige à modifier plusieurs services
- Il n’y a pas de « propriétaire » clair pour chaque table ou ensemble de données
- Les migrations de base de données sont des événements à haut risque
Impact
- Couplage caché : Les services semblent indépendants mais sont liés par la structure de la base de données. Un
ALTER TABLEpeut casser des services dont vous ignoriez même qu’ils utilisaient cette table - Scalabilité limitée : Vous ne pouvez pas mettre à l’échelle la base de données de façon indépendante par service
- Migrations dangereuses : Tout changement de schéma est un risque car vous ne savez pas qui dépend de quoi
- Impossibilité d’évoluer : Chaque service reste lié à la technologie et à la structure de la base de données partagée
Comment le détecter
- Vérifiez si plus d’un service possède des connection strings vers la même base de données
- Recherchez les requêtes qui accèdent à des tables « appartenant » à un autre domaine fonctionnel
- Posez la question : qui est le propriétaire de cette table ? Si la réponse est « plusieurs services », vous avez cet anti-pattern
- Exécutez une analyse de dépendances : cartographiez quels services accèdent à quelles tables
Correction
| Stratégie | Description |
|---|---|
| Database per Service | Chaque service possède sa propre base de données. Seul le service propriétaire peut lire et écrire ses données |
| Exposer les données via API | Si le Service B a besoin de données du Service A, il les demande via l’API de A |
| Événements pour la synchronisation | Utiliser des événements de domaine pour que les services conservent des copies locales des données dont ils ont besoin |
| Outbox Pattern | Publier les événements de manière fiable lorsque les données changent, afin que les autres services se synchronisent |
| Migration incrémentale | N’essayez pas de tout séparer d’un coup. Identifiez les tables les plus problématiques et migrez service par service |
3. God Service (Service Dieu)
Problème
Un seul service concentre trop de responsabilités et devient le point central par lequel passe presque toute la logique du système. Il commence généralement comme un service légitime qui accumule des fonctionnalités parce que « c’est plus facile de l’ajouter ici ».
┌─────────────────────┐
│ God Service │
│ │
│ • Autenticación │
│ • Usuarios │
│ • Pedidos │
│ • Pagos │
│ • Notificaciones │
│ • Reportes │
│ • Inventario │
└──────────┬──────────┘
│
┌────────────────┼────────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│Servicio X│ │Servicio Y│ │Servicio Z│
│(simple) │ │(simple) │ │(simple) │
└──────────┘ └──────────┘ └──────────┘
Todos dependen del God Service
Les signes les plus évidents sont :
- Un service possède nettement plus d’endpoints que les autres
- La plupart des changements fonctionnels nécessitent de modifier ce service
- Le service assume plusieurs responsabilités sans lien entre elles
- L’équipe qui le maintient constitue un goulot d’étranglement permanent
Impact
- Goulot d’étranglement du développement : Toutes les équipes doivent modifier le même service
- Point unique de défaillance : Si le God Service tombe, une grande partie du système cesse de fonctionner
- Scalabilité inégale : Vous ne pouvez pas mettre à l’échelle uniquement les paiements s’ils sont empaquetés avec les rapports
- Complexité croissante : Le service devient difficile à comprendre, à tester et à maintenir
- Déploiements risqués : Chaque déploiement touche à de nombreuses fonctionnalités
Comment le détecter
- Mesurez les lignes de code ou les endpoints par service. Si l’un en a 10x plus que la moyenne, c’est suspect
- Examinez les commits : si un service concentre la majorité des changements de plusieurs équipes, c’est probablement un God Service
- Posez la question : puis-je décrire la responsabilité de ce service en une phrase ? Si vous avez besoin d’une longue liste, il a trop de responsabilités
- Analysez les dépendances : si la majorité des services dépendent d’un seul, c’est un God Service
Correction
| Stratégie | Description |
|---|---|
| Identifier les bounded contexts (DDD) | Cartographier les domaines réels au sein du God Service. Chaque domaine est candidat à devenir un service indépendant |
| Strangler Fig Pattern | Extraire les fonctionnalités une par une, en créant de nouveaux services qui interceptent les appels au God Service |
| Principe de responsabilité unique | Chaque service doit avoir une raison claire d’exister |
| Prioriser par impact | Commencez par extraire la fonctionnalité qui change le plus fréquemment ou qui a des besoins de scalabilité différents |
| API Gateway / BFF | Utiliser une gateway pour router les requests vers des services spécialisés plutôt que vers le God Service |
4. Services bavards (Chatty Services)
Problème
Les services communiquent entre eux avec trop d’appels à granularité fine pour compléter une seule opération. Au lieu d’une requête qui récupère toute l’information nécessaire, un service effectue de multiples petits appels.
Servicio A necesita mostrar el perfil de un usuario:
Servicio A → Servicio Usuarios: GET /users/123
Servicio A → Servicio Usuarios: GET /users/123/preferences
Servicio A → Servicio Pedidos: GET /orders?user=123
Servicio A → Servicio Pedidos: GET /orders/456/details
Servicio A → Servicio Pedidos: GET /orders/789/details
Servicio A → Servicio Pagos: GET /payments?user=123
Servicio A → Servicio Reviews: GET /reviews?user=123
Total: 7 llamadas de red para una sola pantalla
Les signes les plus évidents sont :
- Une opération métier génère plus de 3-4 appels entre services
- La latence est élevée alors que chaque service individuel répond rapidement
- Les logs montrent des rafales d’appels corrélés pour un seul request
Impact
- Latence cumulée : 7 appels de 50 ms chacun totalisent 350 ms dans le meilleur des cas (séquentiel)
- Fragilité : Avec 7 appels et 99 % de disponibilité par service, la disponibilité effective descend à ~93 %
- Charge réseau excessive : Le trafic interne peut dépasser le trafic externe
- Difficulté de débogage : Retracer une erreur à travers plusieurs appels nécessite une traçabilité distribuée
Comment le détecter
- Comptez les appels entre services que génère un seul request de l’utilisateur
- Mesurez la latence end-to-end vs la latence individuelle de chaque service
- Vérifiez si les services exposent des endpoints très granulaires
Correction
| Stratégie | Description |
|---|---|
| BFF (Backend for Frontend) | Créer un service intermédiaire qui agrège les données de plusieurs services en une seule réponse |
| Endpoints agrégés | Concevoir des endpoints qui renvoient toute l’information nécessaire à un cas d’usage |
| Cache local | Conserver des copies locales des données qui changent peu |
| Appels en parallèle | Si les appels sont indépendants, les exécuter en parallèle |
| Événements pour données dérivées | Utiliser des événements pour que chaque service maintienne une vue matérialisée des données dont il a besoin |
5. Modèle anémique (Anemic Domain Model)
Problème
Les entités du domaine sont des conteneurs de données sans comportement — elles n’ont que des getters et des setters. Toute la logique métier vit dans des services externes qui manipulent ces entités passives.
// ❌ Modelo anémico
class Pedido {
id: string;
estado: string;
líneas: LíneaPedido[];
total: number;
// Solo getters y setters, sin lógica
}
class PedidoService {
confirmar(pedido: Pedido) {
if (pedido.estado !== 'borrador') throw new Error('...');
if (pedido.líneas.length === 0) throw new Error('...');
pedido.estado = 'confirmado';
pedido.total = this.calcularTotal(pedido.líneas);
// Toda la lógica está aquí, no en Pedido
}
}
// ✅ Modelo rico
class Pedido {
confirmar() {
if (this.estado !== 'borrador')
throw new Error('Solo se puede confirmar un pedido en borrador');
if (this.líneas.length === 0)
throw new Error('El pedido debe tener al menos una línea');
this.estado = 'confirmado';
this.total = this.calcularTotal();
this.registrarEvento(new PedidoConfirmado(this.id));
}
}
Impact
- Logique dispersée : Les règles métier sont dupliquées dans plusieurs services
- Difficile à tester : Les tests doivent configurer des services externes pour valider des règles simples
- Violation de l’encapsulation : N’importe qui peut modifier l’état de l’entité sans passer par les règles
- Difficile à découvrir : Il n’est pas évident de savoir quelles opérations sont valides sur une entité
Correction
| Stratégie | Description |
|---|---|
| Déplacer la logique vers les entités | Les règles métier qui opèrent sur les données d’une entité doivent vivre dans l’entité |
| Appliquer le DDD | Utiliser des agrégats avec des invariants qui protègent la cohérence |
| Value Objects | Encapsuler des concepts comme Argent, Email, Adresse avec leur propre validation |
| Domain Events | Les entités publient des événements lorsqu’elles changent d’état |
6. Intégration par base de données (Database Integration)
Problème
Les services s’intègrent en lisant et écrivant dans les tables d’autres services au lieu de communiquer par API ou par événements. C’est une variante plus subtile de la base de données partagée.
Servicio A necesita saber si un usuario es premium:
❌ SELECT tipo FROM usuarios WHERE id = 123
(accede directamente a la tabla del Servicio B)
✅ GET /api/users/123/subscription
(consulta al Servicio B por su API)
Impact
- Couplage invisible : Il n’y a pas de contrat explicite, seulement une connaissance implicite de la structure des tables
- Impossible à versionner : Vous ne pouvez pas faire évoluer le schéma sans casser les consommateurs cachés
- Aucun contrôle d’accès : N’importe quel service peut lire ou écrire n’importe quelle donnée
Correction
| Stratégie | Description |
|---|---|
| API explicites | Chaque service expose une API pour accéder à ses données |
| Événements de domaine | Publier les changements sous forme d’événements pour que d’autres services s’y abonnent |
| ACL Pattern | Créer une couche de traduction entre le modèle interne et les consommateurs |
Tableau comparatif
| Anti-pattern | Signe principal | Risque majeur | Première action |
|---|---|---|---|
| Monolithe distribué | Vous ne pouvez pas déployer un service seul | Complexité sans bénéfice | Définir des contrats et des bounded contexts |
| Base de données partagée | Plusieurs services accèdent aux mêmes tables | Migrations dangereuses | Attribuer l’ownership des tables |
| God Service | Un service concentre toute la logique | Point unique de défaillance | Identifier les domaines à extraire |
| Services bavards | Beaucoup d’appels pour une opération | Latence et fragilité | Créer des endpoints agrégés ou un BFF |
| Modèle anémique | Entités sans comportement | Logique dispersée et dupliquée | Déplacer la logique vers les entités (DDD) |
| Intégration par BD | Requêtes directes sur les tables d’autres services | Couplage invisible | Exposer des API explicites |
Comment prévenir les anti-patterns
Les anti-patterns apparaissent rarement d’un coup. Ils s’accumulent progressivement. Quelques pratiques qui aident à les prévenir :
- Revues d’architecture régulières : Évaluez régulièrement si les services conservent leur indépendance
- Fitness functions : Définissez des métriques automatisées qui détectent le couplage (nombre de dépendances, taille des services, fréquence des déploiements conjoints)
- ADR (Architecture Decision Records) : Documentez les décisions architecturales et leurs raisons
- Ownership clair : Chaque service, table et API doit avoir une équipe responsable
- Principe du rasoir : Si vous ne pouvez pas justifier pourquoi un service est indépendant, il ne devrait probablement pas l’être
- Suivi du couplage : Mesurez combien de services sont déployés ensemble, combien d’appels synchrones existent entre services, et combien de tables sont partagées
Relation avec d’autres patterns
Les anti-patterns se préviennent et se corrigent en appliquant les bons patterns :
Anti-patrón Patrón que lo corrige
─────────────────────────────────────────────────────
Monolito Distribuido → DDD (Bounded Contexts)
→ Event-Driven Architecture
→ ACL Pattern
Base de Datos Compartida → Database per Service
→ Outbox Pattern (sincronización)
→ Event-Driven Architecture
God Service → Microservices Pattern
→ DDD (descomposición por dominio)
→ API Gateway / BFF
Servicios Parlanchines → BFF Pattern
→ API Gateway Pattern
→ Event-Driven Architecture
Modelo Anémico → DDD (diseño táctico)
→ Aggregates + Domain Events
Integración por BD → ACL Pattern
→ Outbox Pattern
→ Shared Kernel (controlado)
Prochaines étapes
Maintenant que vous connaissez les anti-patterns les plus courants, approfondissez les patterns qui les préviennent : le DDD pour définir des bounded contexts clairs, le Microservices Pattern pour décomposer correctement, l’Event-Driven Architecture pour découpler les services, et le BFF Pattern pour résoudre le problème des services bavards.