Read in English · Leer en español · Auf Deutsch lesen
Section 5.2 sur 5.8 · Intermédiaire · 24 min de lecture
Architecture en couches
Leçon vidéo
5 min · narré en anglais, avec sous-titresÉcouter seulement (audio)
Lire la transcription
L’architecture en couches est le style le plus utilisé pour les applications métier, souvent choisi par défaut plutôt que par décision. Dans cette leçon, vous allez découvrir comment elle fonctionne, ce qu’elle coûte, et comment empêcher ses règles de s’éroder.
Pensez à un restaurant. Les clients ne parlent qu’aux serveurs. Les serveurs transmettent les commandes à la cuisine et n’entrent jamais eux-mêmes dans la réserve. Les cuisiniers obtiennent les ingrédients auprès du personnel de la réserve, et seul le personnel de la réserve traite avec les fournisseurs. Chaque groupe a une seule tâche et ne parle qu’au groupe voisin, si bien que vous pouvez former à nouveau les serveurs sans toucher à la cuisine. Une application en couches fonctionne de la même manière.
Voici une application de commande de repas en couches. Une requête venue du navigateur ou de l’app arrive à la couche de présentation, qui gère les écrans, le HTTP et le JSON. La couche métier contient les règles et les cas d’utilisation : comment le prix d’une commande est calculé, et quand elle peut être annulée. La couche de persistance transforme les objets métier en données stockées et inversement. Elle seule parle à la base de données. Ces couches sont fermées : une requête doit les traverser. La présentation ne peut pas passer outre la couche métier pour atteindre la persistance, même quand ce serait plus rapide à écrire. Et aucune couche ne peut jamais appeler vers le haut. La couche de services, pour les e-mails, les reçus PDF et les enregistrements d’audit, est ouverte. Seules certaines opérations en ont besoin, si bien que la couche métier peut l’appeler, ou passer directement à côté pour atteindre la persistance.
Les couches fermées vous donnent des couches d’isolation. Comme la présentation ne connaît que l’interface de la couche métier, l’équipe peut restructurer les tables, voire changer de produit de base de données, sans toucher à un seul écran. Chaque exception qui permet à une couche d’en sauter une autre affaiblit cette isolation. Si les contrôleurs interrogent directement la base de données, un changement de schéma se propage désormais aussi à la couche de présentation.
Suivons une requête. Un client appuie sur Commander. La couche de présentation analyse la requête, vérifie que l’utilisateur est connecté, et appelle le service de commande. Elle ne sait rien des tables ni des prix. La couche métier vérifie que le restaurant est ouvert, calcule le prix du panier, et demande au repository d’enregistrer la commande. Elle utilise aussi la couche de services ouverte pour envoyer un reçu par e-mail. La couche de persistance fait correspondre la commande à des lignes et les écrit dans une seule transaction de base de données. PostgreSQL valide, et le résultat remonte. Le client voit son numéro de commande. Maintenant, l’équipe produit demande un pourboire pour le livreur. Il faut un champ dans la requête, une règle de pourboire maximal, une nouvelle colonne et une migration. Quatre couches pour une petite fonctionnalité : c’est le coût du partitionnement technique.
L’organisation en couches a un anti-patron classique, le gouffre d’architecture : des requêtes qui traversent des couches qui ne font que les transmettre. Consulter une carte en est l’exemple typique. Le contrôleur relaie, le service relaie, et seul le repository fait un vrai travail. Comparez avec la prise de commande, où chaque couche ajoute un comportement : valider, calculer le prix et vérifier les règles, puis enregistrer dans une transaction. Un certain relais est le prix de l’isolation. En règle générale, environ un cinquième des requêtes, c’est normal. Si la plupart des requêtes sont de simples relais, l’organisation en couches n’est probablement pas le bon style.
Le style obtient une note élevée en simplicité et en faible coût : il est familier à presque tous les développeurs, avec une seule base de code, un seul déploiement et une seule base de données. Il obtient une note basse en déployabilité, en scalabilité et en tolérance aux pannes. Tout changement redéploie l’ensemble, l’application entière passe à l’échelle d’un seul bloc, et une seule fuite de mémoire peut tout faire tomber. Il convient bien aux petites et moyennes applications construites par une seule équipe, et aux applications qui consistent surtout en formulaires sur des données. Il convient mal comme niveau supérieur d’un grand système à plusieurs équipes, car chaque équipe travaille dans chaque couche. L’étape suivante habituelle est un monolithe modulaire, avec des couches à l’intérieur de chaque module.
Les règles des couches se dégradent vite. Quelqu’un importe le repository directement dans un contrôleur pour gagner vingt minutes, personne ne le remarque, et le développeur suivant le copie. La défense est un test d’architecture : une vérification automatisée des dépendances qui s’exécute dans le build, et le fait échouer lorsqu’une règle est enfreinte. En Python, import-linter lit les règles dans un fichier de configuration. En Java, ArchUnit les exprime sous forme de test unitaire. Commencez par enregistrer les violations existantes comme des exceptions connues, afin que seules les nouvelles fassent échouer le build. Puis réduisez les exceptions au fil du temps.
La section complète présente le tableau des caractéristiques, d’autres anti-patrons et une configuration import-linter fonctionnelle. Ensuite, dans la section cinq point trois, nous gardons un seul déploiement mais le découpons par domaine métier : le monolithe modulaire. À très vite.
Ce qu’il faut savoir
- Une architecture en couches organise le code en couches horizontales selon le rôle technique, généralement présentation, métier, persistance et base de données, et ne permet à chaque couche de dépendre que des couches situées en dessous.
- Les couches fermées ne doivent pas être contournées, ce qui crée des couches d’isolation : un changement à l’intérieur d’une couche ne se propage pas aux autres tant que son interface tient. Une couche ouverte, comme une couche de services partagés, peut être contournée.
- L’organisation en couches est un partitionnement technique : une fonctionnalité métier typique touche donc toutes les couches, ce qui ralentit le changement et incite à des structures d’équipes découpées par couche.
- L’anti-patron du gouffre d’architecture désigne les requêtes qui traversent des couches qui ne font que les transmettre ; en règle générale, une minorité de telles requêtes est normale, une large majorité suggère que le style n’est pas le bon.
- Le style obtient une note élevée en simplicité et en faible coût, et une note basse en déployabilité, scalabilité, élasticité et tolérance aux pannes, car le système entier est une seule unité de déploiement.
- Les règles des couches s’érodent si on ne les fait pas respecter automatiquement, par exemple avec import-linter en Python ou ArchUnit en Java, sous forme de tests qui font échouer le build.
Définition et origine
Une architecture en couches organise une application en couches horizontales, dont chacune a une responsabilité technique unique, offre des services à la couche supérieure et utilise les services de la couche inférieure. C’est le style le plus utilisé pour les applications métier, souvent par défaut plutôt que par décision, car la plupart des frameworks web génèrent un projet avec des dossiers pour les vues, la logique métier et les modèles.
L’idée est ancienne et vient du logiciel système. Edsger Dijkstra a décrit le système d’exploitation multiprogrammé THE (Communications of the ACM, 1968) comme une hiérarchie de niveaux, chacun construit uniquement sur ceux qui se trouvent en dessous, afin que chaque niveau puisse être raisonné et testé isolément. Les piles de protocoles réseau comme le modèle de référence OSI appliquent le même principe. Dans le logiciel de gestion, l’organisation en couches est arrivée avec les systèmes client-serveur et à trois niveaux des années 1990, et POSA (1996) l’a cataloguée sous le nom de patron Couches (Layers). Garlan et Shaw ont rangé les systèmes en couches parmi leurs styles de base, avec la contrainte que les interactions soient limitées aux couches adjacentes.
L’application métier typique comporte quatre couches. La couche de présentation gère l’interface utilisateur et le protocole : requêtes HTTP, écrans, JSON. La couche métier contient les règles et les cas d’utilisation : comment le prix d’une commande est calculé, quand elle peut être annulée. La couche de persistance transforme les objets métier en données stockées et inversement, généralement au moyen de repositories et d’un outil de mapping objet-relationnel (ORM). La couche base de données est la base de données elle-même. Les petites applications fusionnent parfois métier et persistance ; les plus grandes ajoutent des couches, comme une couche application qui coordonne les cas d’utilisation au-dessus d’une couche domaine qui contient le modèle central. Le style ne fixe pas le nombre de couches ; il fixe le sens des dépendances.
Une application de commande de repas en couches
Fonctionnement : couches fermées, couches ouvertes et isolation
Chaque couche peut être fermée ou ouverte. Une requête doit traverser une couche fermée : la couche de présentation ne peut pas passer outre la couche métier pour atteindre la couche de persistance, même quand ce serait plus rapide à écrire. Une couche ouverte peut être contournée. L’exemple habituel est une couche de services techniques partagés, comme l’envoi d’e-mails, la génération de reçus PDF ou l’écriture d’enregistrements d’audit. Seules certaines opérations métier en ont besoin : forcer chaque requête à traverser cette couche ajouterait donc du code de relais inutile. Dans le diagramme, la couche métier peut appeler la couche de services ou passer directement à côté pour atteindre la persistance.
Les couches fermées créent des couches d’isolation. Comme la couche de présentation ne connaît que l’interface de la couche métier, l’équipe peut remplacer l’outil de mapping objet-relationnel, restructurer les tables ou passer d’un produit de base de données à un autre sans toucher à un seul contrôleur ni à un seul template, tant que l’interface de la couche métier reste la même. C’est le masquage de l’information (David Parnas, 1972) appliqué à toute une strate du code, et c’est le principal avantage qu’offre le style. Chaque couche ouverte, et chaque exception qui permet à une couche d’en sauter une autre, l’affaiblit : si les contrôleurs interrogent directement la base de données, un changement de schéma se propage désormais aussi à la couche de présentation.
Le sens des dépendances est l’autre moitié de la règle. Les couches inférieures ne doivent rien savoir des couches supérieures : la couche de persistance ne doit jamais importer un contrôleur, et la couche métier ne doit jamais construire de HTML. Lorsqu’une couche inférieure doit notifier une couche supérieure, elle le fait au moyen d’une abstraction qu’elle possède, comme une interface de rappel (callback) ou un événement, afin que la dépendance pointe toujours vers le bas dans le code. La section 3.4 traite en profondeur du sens des dépendances, des cycles et de l’organisation en couches stricte ou souple.
Il est utile de bien distinguer couches et niveaux. Une couche est un regroupement logique dans le code. Un niveau (tier) est une frontière physique à l’exécution, comme un serveur ou un processus séparé. Un système classique à trois niveaux fait tourner le navigateur, un serveur d’applications et un serveur de base de données comme trois niveaux, alors que le serveur d’applications à lui seul peut contenir trois ou quatre couches. Transformer une couche en niveau place un réseau entre deux couches qui s’appelaient auparavant en mémoire, ce qui multiplie le coût de chaque appel ; cela vaut rarement la peine en soi.
Exemple : une application web classique
Prenons le site de commande d’une entreprise régionale de livraison de repas : les clients parcourent les cartes, passent commande et paient ; le personnel des restaurants voit les commandes arriver. Une équipe de cinq personnes le construit avec un framework web courant et une base de données PostgreSQL. Une structure en couches est le point de départ évident, et l’organisation du code rend les couches visibles.
foodshop/
presentation/ # HTTP only: parse requests, render responses
order_views.py
menu_views.py
templates/
business/ # rules and use cases; no HTTP, no SQL
order_service.py
pricing.py
services/ # open layer: shared technical services
email.py
receipts.py
persistence/ # all database access
order_repository.py
menu_repository.py
models.py
settings.pyExemple commenté
Exemple commenté : suivre une requête à travers les couches
Un client appuie sur Commander. La requête arrive sous la forme POST /orders, avec le panier et l’adresse de livraison.
| Présentation | order_views.place_order analyse le JSON, vérifie qu’il est bien formé et que l’utilisateur est connecté, et appelle order_service.place(customer_id, items, address). Elle ne sait rien des tables ni des prix. |
|---|---|
| Métier | order_service.place vérifie que le restaurant est ouvert, calcule le prix du panier avec pricing.py (remises, frais de livraison, taxes) et demande à order_repository d’enregistrer la commande. Il appelle receipts.send dans la couche de services ouverte pour envoyer un reçu par e-mail. |
| Persistance | order_repository.save fait correspondre la commande à des lignes dans orders et order_lines et les écrit dans une seule transaction de base de données. |
| Base | PostgreSQL valide la transaction et rend la main. Le résultat remonte : le service renvoie la nouvelle commande, la vue renvoie 201 Created avec le numéro de commande. |
| Demande de changement | L’équipe produit demande un pourboire pour le livreur. Le développeur ajoute un champ de pourboire à la requête et à la réponse (présentation), une règle de pourboire maximal et le pourboire dans le total (métier), ainsi qu’une colonne et un mapping (persistance), plus une migration (base de données). Quatre couches pour une petite fonctionnalité. |
Chaque couche a accompli une seule sorte de tâche, et chacune pouvait être testée seule, la couche inférieure étant remplacée par une doublure de test.
Le pourboire montre le coût du partitionnement technique : même un petit changement métier traverse chaque couche, si bien qu’il touche plus de fichiers, plus de relecteurs et, si les équipes sont découpées par couche, plus d’équipes.
Les conséquences du partitionnement technique
Comme l’organisation en couches est un partitionnement technique, le domaine métier est éparpillé à travers les couches. Il n’existe pas d’endroit unique appelé livraison
: il y a des contrôleurs de livraison, des services de livraison et des repositories de livraison, entremêlés avec tout le reste dans chaque couche. Un développeur qui veut comprendre le fonctionnement des pourboires doit tous les parcourir. À mesure que le système grandit, chaque couche devient un vaste ensemble peu cohésif de code sans rapport : la couche métier contient côte à côte les règles des commandes, des paiements, des cartes et des livreurs, et rien n’empêche le code de paiement d’appeler le code des cartes en profondeur. Avec le temps, les couches restent propres, mais l’intérieur de chaque couche devient une grosse boule de boue.
Il y a aussi un coût organisationnel. Organiser les équipes par couche (une équipe frontend, une équipe backend, une équipe base de données) signifie que presque chaque fonctionnalité exige un passage de relais et une file d’attente entre équipes, que la loi de Conway grave ensuite dans l’architecture. Martin Fowler qualifie pour cette raison les équipes organisées par couche d’anti-patron : les développeurs n’ont pas besoin d’être full-stack, mais les équipes, si. Une architecture en couches n’oblige pas à découper les équipes par couche, mais elle en fait la voie de moindre résistance.
Enfin, un monolithe en couches est un seul quantum d’architecture : une seule unité de déploiement et généralement une seule base de données. Chaque changement, aussi petit soit-il, implique de construire, tester et redéployer l’application entière, et l’application entière passe à l’échelle et tombe en panne d’un seul bloc. C’est souvent parfaitement acceptable, mais cela fixe un plafond.
Le gouffre d’architecture et autres anti-patrons
L’anti-patron du gouffre d’architecture, un nom popularisé par Mark Richards, désigne les requêtes qui traversent des couches qui ne font rien d’autre que les transmettre. Consulter une carte en est le cas typique : le contrôleur appelle une méthode de service qui appelle une méthode de repository qui exécute une requête, et le contrôleur comme le service n’ajoutent rien. Chaque couche de simple relais ajoute du code à écrire, à tester et à lire, ainsi qu’un peu de latence. Un certain relais est le prix de l’isolation et ne pose pas de problème. Richards propose une règle empirique : si environ un cinquième des requêtes sont de simples relais, c’est normal ; si c’est le cas de la plupart, l’application consiste probablement surtout en accès aux données avec peu de logique métier, et l’organisation en couches n’est pas le bon style, ou bien les couches devraient être ouvertes. Ouvrir des couches fait cependant renoncer à l’isolation : décidez-le donc délibérément et consignez pourquoi.
D’autres anti-patrons courants méritent d’être reconnus tôt. Les couches qui fuient : du SQL dans les contrôleurs, des objets de requête HTTP passés aux services, ou des entités ORM rendues directement en JSON, si bien qu’un changement dans une couche se propage aux autres. Les dépendances montantes : un repository qui appelle un service, ou un objet de domaine qui met en forme du HTML. La couche métier anémique : des services qui ne font que déplacer des données pendant que les vraies règles vivent dans les contrôleurs ou dans des procédures stockées ; Fowler a nommé en 2003 l’anti-patron voisin Anemic Domain Model (modèle de domaine anémique). Le service divin : un unique OrderService de plusieurs milliers de lignes que tous les autres services appellent. La couche utilitaire dont tout dépend : un paquet common qui accumule peu à peu de la logique métier et couple tout à tout.
Gouffre ou organisation en couches utile
Caractéristiques du style en couches
| Caractéristique | Évaluation | Justification |
|---|---|---|
| Simplicité | Élevé | Familier à presque tous les développeurs et structure par défaut de la plupart des frameworks ; facile à expliquer et à démarrer. |
| Coût global | Élevé (peu coûteux) | Une seule base de code, un seul déploiement, une seule base de données ; infrastructure et exploitation minimales. |
| Testabilité | Moyen | Chaque couche peut être testée en remplaçant la couche inférieure par une doublure de test, mais des tests réalistes exigent souvent toute la pile et une base de données. |
| Modifiabilité pour les changements métier | Bas à moyen | Les changements techniques restent dans une couche, mais la plupart des changements de fonctionnalités traversent toutes les couches. |
| Déployabilité | Bas | Tout changement implique de redéployer l’application entière ; le risque de mise en production croît avec la taille. |
| Scalabilité et élasticité | Bas | L’application entière passe à l’échelle comme une seule unité ; vous ne pouvez pas faire passer à l’échelle uniquement la partie chargée, et le démarrage d’une grande application ralentit la mise à l’échelle élastique. |
| Tolérance aux pannes | Bas | Une seule fuite de mémoire ou une requête qui s’emballe peut faire tomber tout le processus ; il n’y a aucune isolation entre les fonctionnalités. |
| Performances | Moyen | Les appels internes au processus sont rapides, mais les couches supplémentaires et le mapping des objets ajoutent un surcoût, et les gouffres en ajoutent davantage. |
| Évolutivité à grande échelle | Bas | La faible cohésion à l’intérieur de chaque couche et le couplage entre fonctionnalités rendent les grands systèmes en couches difficiles à restructurer. |
Quand il convient, et quand il ne convient pas
L’architecture en couches est un bon choix pour les petites et moyennes applications construites par une seule équipe, pour les produits dont le domaine n’est pas encore bien compris (le partitionnement par domaine exige des frontières que vous ne connaissez pas encore), pour les budgets et les délais serrés, et pour les applications qui consistent surtout en formulaires sur des données, avec des règles modestes. C’est aussi une structure interne raisonnable pour un module ou un service unique qui fait lui-même partie d’un système partitionné par domaine : un module delivery en couches à l’intérieur d’un monolithe modulaire profite du meilleur des deux.
C’est un mauvais choix comme structure de premier niveau d’un grand système à plusieurs équipes, car chaque équipe travaille dans chaque couche et les fonctionnalités entrent en collision. C’est un mauvais choix lorsque des parties du système ont besoin d’une scalabilité ou d’une disponibilité très différentes, car il n’y a qu’un seul quantum. Et c’est un mauvais choix lorsque la logique métier est riche et centrale, car les règles sont dispersées entre contrôleurs, services et procédures stockées ; dans ce cas, une structure interne centrée sur le domaine comme l’architecture hexagonale ou Clean Architecture garde le cœur indépendant de la base de données et du framework web.
Le chemin d’évolution le plus courant va d’un monolithe en couches à un monolithe modulaire : conserver un seul déploiement, mais réorganiser le niveau supérieur par domaine et garder des couches à l’intérieur de chaque module.
Faire respecter les règles des couches dans le code
Les règles des couches se dégradent vite. Sous la pression des délais, un développeur importe le repository directement dans un contrôleur parce que cela lui fait gagner vingt minutes, personne ne le remarque en revue, et le développeur suivant copie ce modèle. La défense est un test d’architecture : une vérification automatisée des dépendances qui s’exécute dans le build et le fait échouer lorsqu’une règle est enfreinte, ce qui fait de la règle une fonction d’aptitude.
En Python, import-linter lit des contrats dans un fichier de configuration. Son contrat layers énumère les couches de la plus haute à la plus basse et interdit à toute couche inférieure d’importer une couche supérieure. À lui seul, il ne rend pas les couches fermées (une couche supérieure peut importer n’importe quelle couche inférieure) : un contrat forbidden ajoute donc la règle selon laquelle la présentation ne doit pas importer directement la persistance ; il faut activer allow_indirect_imports, sans quoi la chaîne normale de la présentation vers la persistance en passant par le métier compterait comme une violation. En Java, ArchUnit exprime les mêmes règles sous forme de test unitaire : layeredArchitecture() déclare les couches par paquet et indique, par exemple, que la couche de persistance ne peut être accédée que par la couche de services. Des outils similaires existent pour d’autres écosystèmes, comme dependency-cruiser pour JavaScript et TypeScript.
Deux pratiques font tenir les vérifications. Introduisez-les en enregistrant les violations existantes comme des exceptions connues, afin que le build passe immédiatement au vert et que seules les nouvelles violations le fassent échouer ; puis réduisez les exceptions au fil du temps. Et gardez la configuration à côté du code et relisez ses modifications comme du code, car assouplir une règle est une décision d’architecture qui mérite un enregistrement de décision.
[importlinter]
root_package = foodshop
[importlinter:contract:layers]
name = Layers only depend downwards
type = layers
layers =
foodshop.presentation
foodshop.business
foodshop.services
foodshop.persistence
[importlinter:contract:closed-business]
name = Presentation must go through business
type = forbidden
source_modules = foodshop.presentation
forbidden_modules = foodshop.persistence
# presentation -> business -> persistence is the normal path
allow_indirect_imports = TrueVérifiez votre compréhension
Quelle est la différence entre une couche fermée et une couche ouverte ?
Une requête doit traverser une couche fermée et ne peut pas la sauter ; une couche ouverte peut être contournée par la couche supérieure, ce qui est typique d’une couche de services partagés dont seules certaines requêtes ont besoin.
Pourquoi une application en couches a-t-elle tendance à exiger des changements dans chaque couche pour une nouvelle fonctionnalité ?
Parce qu’elle est partitionnée techniquement : le code de chaque capacité métier est réparti entre les couches de présentation, métier et persistance, et la plupart des fonctionnalités sont définies en termes métier.
Quelle est la différence entre une couche et un niveau (tier) ?
Une couche est un regroupement logique du code par responsabilité ; un niveau est une frontière physique à l’exécution, comme un serveur séparé. Plusieurs couches tournent généralement à l’intérieur d’un seul niveau.
Pourquoi un contrat layers d’import-linter ne rend-il pas, à lui seul, les couches fermées ?
Il interdit seulement aux couches inférieures d’importer les couches supérieures ; une couche supérieure peut toujours importer n’importe quelle couche inférieure, si bien qu’un contrat forbidden séparé est nécessaire pour empêcher la présentation d’importer la persistance.
Sources et lectures complémentaires
Une question sur cette section ? ArchitectBot, l’assistant d’étude IA du cours, répond d’abord à partir du cours.
Demander : « La plupart de nos requêtes passent simplement du contrôleur au service puis au repository sans rien faire. Devons-nous supprimer la couche de services ? »