Architecture logicielle
Architecture en couches et n-tiers : tiers, couches et règle de dépendance
Débutant11 min de lectureMis à jour
Cette page a été traduite de l'anglais avec l'aide de l'IA, puis vérifiée par rapport à l'original. Lire la version anglaise
Le découpage en couches est la première décision d'architecture que prennent la plupart des systèmes, et l'une des plus mal comprises. À l'issue de ce guide, vous saurez distinguer les tiers physiques des couches logiques, choisir une structure de couches raisonnable pour une application de gestion, orienter les dépendances dans le bon sens et reconnaître les anti-patterns qui transforment une base de code en couches en labyrinthe.
Ce que vous allez apprendre
- Expliquer la différence entre un tier physique et une couche logique, et pourquoi on les confond si souvent
- Décrire ce qui distingue le client-serveur à deux tiers, le trois-tiers et le n-tiers, et le problème résolu à chaque étape
- Structurer les couches présentation, application, domaine et infrastructure avec des dépendances orientées vers le centre
- Documenter une architecture applicative avec une vue des composants et une matrice des flux
- Repérer les anti-patterns du découpage en couches : entités qui fuient, services anémiques, logique métier dans les contrôleurs
Presque toutes les applications de gestion que vous ouvrirez sont découpées en couches, et la plupart le sont mal. Introduire des couches ne coûte presque rien ; les corriger coûte cher, car une structure qui laisse les règles métier fuir dans les contrôleurs ou le SQL fuir dans le domaine se propage à des milliers de classes avant que quiconque ne s'en aperçoive. Pour bien faire, il faut comprendre trois notions que l'on confond sans cesse : les tiers physiques, les couches logiques et le sens dans lequel les dépendances ont le droit de pointer.
Les tiers sont physiques, les couches sont logiques
Un tier est une frontière de déploiement : un processus, une machine ou un cluster qui dialogue avec un autre à travers le réseau. Une couche est une frontière dans le code : un ensemble de packages avec une responsabilité définie et des règles sur ce qu'il a le droit d'appeler. Les deux notions sont indépendantes. Une application desktop mono-processus peut compter cinq couches bien nettes ; un système distribué sur six tiers peut n'avoir aucun découpage en couches digne de ce nom.
La confusion vient de ce que le déploiement trois-tiers classique — un navigateur, un serveur d'applications et une base de données — se trouve coïncider avec trois couches évidentes : présentation, logique métier et accès aux données. Cette coïncidence a rendu « trois-tiers » et « trois couches » interchangeables dans le langage courant, et elle masque une vérité importante : déplacer du code vers un autre tier est une décision de réseau et d'exploitation, le déplacer vers une autre couche est une décision de conception. Vous pouvez changer l'un sans toucher à l'autre.
L'évolution des tiers
Le client-serveur à deux tiers
Dans le modèle à deux tiers, un client lourd contient l'interface utilisateur et toute la logique métier, et parle directement à une base de données partagée via un protocole de base de données. Les règles métier vivent dans le programme client, parfois dans des procédures stockées. Le modèle est simple et rapide à mettre en œuvre, ce qui explique sa domination dans les années 1990, mais il souffre de problèmes structurels :
- Chaque client a besoin d'une connexion à la base, qui devient donc la limite de montée en charge.
- Changer une règle impose de redéployer chaque poste client.
- Le schéma de la base devient le contrat d'intégration entre applications, si bien que personne n'ose plus y toucher.
- La sécurité s'applique au niveau de l'utilisateur de base de données, bien trop grossier pour gérer les autorisations métier.
Le trois-tiers
Les déploiements trois-tiers ont inséré un serveur d'applications entre les clients et la base de données. Le client est devenu léger (un navigateur, à terme), la logique métier a migré vers le tier intermédiaire et la base s'est retrouvée cachée derrière lui. Ce changement a réglé le problème des connexions (le tier intermédiaire les mutualise dans un pool), celui du déploiement (un seul serveur à mettre à jour) et celui de l'intégration (les autres applications appellent l'interface du tier intermédiaire au lieu d'attaquer les tables). Il a aussi fait naître de nouvelles préoccupations — sessions, pools de connexions, appels distants — que les plateformes de serveurs d'applications d'entreprise des années 2000 ont été conçues pour prendre en charge.
Le n-tiers
Le « n-tiers », c'est ce qui se produit quand le tier intermédiaire se scinde à son tour : un serveur web qui sert les ressources statiques et termine le TLS, un serveur d'applications, une couche d'intégration distincte qui encapsule les systèmes legacy ou les API des partenaires, un tier de cache, un broker de messages, une ou plusieurs bases de données. Chaque tier supplémentaire apporte de l'isolation, une montée en charge indépendante ou une frontière de sécurité, et coûte un saut réseau, un mode de défaillance et un composant de plus à exploiter.
| Modèle | Où vit la logique métier | Ce qu'il a résolu | Ce qu'il coûte |
|---|---|---|---|
| Deux tiers | Programme client, procédures stockées | Simplicité, accès direct aux données | Montée en charge, déploiement, couplage au schéma |
| Trois tiers | Serveur d'applications | Pool de connexions, règles centralisées, schéma masqué | Sessions, appels distants, un serveur à faire tourner |
| N-tiers | Tiers application et intégration | Zones de sécurité, montée en charge indépendante, isolation du legacy | Latence à chaque saut, davantage de modes de défaillance, charge d'exploitation |
Les quatre niveaux d'une description d'architecture
Pour décrire un système, les architectes travaillent généralement à quatre niveaux, chacun répondant à une question différente. Les nommer évite ces réunions où l'un débat de serveurs pendant que l'autre débat de packages.
- Architecture fonctionnelle — les capacités métier que le système fournit et la façon dont elles sont regroupées, dans un vocabulaire que le métier comprend. Aucune technologie n'apparaît à ce niveau.
- Architecture applicative — les applications et composants qui existent, lequel est propriétaire de quelles données et comment ils s'appellent entre eux. C'est ici que l'on dessine le diagramme de composants et la matrice des flux.
- Architecture technique (infrastructure) — les serveurs, zones, réseaux, répartiteurs de charge et bases de données qui hébergent ces composants, avec leurs politiques de disponibilité et de sécurité.
- Architecture logicielle — l'organisation du code à l'intérieur d'un composant : frameworks, modules, couches, règles de codage.
Les couches relèvent du quatrième niveau ; les tiers, du troisième. La matrice des flux, au deuxième niveau, est l'artefact qui fait le lien entre les deux, et elle vaut la peine d'être produite pour tout système comptant plus d'une poignée de composants :
| Réf. | Protocole | Appelant | Appelé | Sécurité | Tracé |
|---|---|---|---|---|---|
| F1 | HTTPS | Navigateur du client | Front web | TLS, cookie de session | oui |
| F2 | HTTP/JSON | Front web | Service commandes | mTLS dans la zone | oui |
| F3 | JDBC | Service commandes | Base des commandes | Identifiants de base issus du coffre-fort | non |
| F4 | HTTPS | Service commandes | Prestataire de paiement | OAuth 2.0 client credentials | oui |
Chaque ligne devient une règle de pare-feu, une sonde de supervision et un timeout potentiel. Les écrire tôt, c'est toute la différence entre une architecture et un dessin.
Les couches logiques à l'intérieur d'un service
À l'intérieur d'un composant déployable, la structure conventionnelle compte quatre couches. Leurs noms varient ; leurs responsabilités, non.
- Présentation (ou interface) — reçoit les requêtes, valide leur forme, les convertit en appels vers la couche inférieure et produit les réponses. Contrôleurs HTTP, listeners de messages, commandes CLI et tâches planifiées vivent tous ici. Elle connaît les codes de statut HTTP et le JSON ; aucune couche en dessous ne les connaît.
- Application — orchestre un cas d'usage : charge les objets nécessaires, invoque le comportement du domaine, gère la transaction, publie des événements. Elle ne contient aucune règle métier propre, seulement de l'enchaînement.
- Domaine — les concepts métier et les règles qui les gouvernent : ce qu'est une commande valide, quand une remise s'applique, ce qui se passe en cas de rupture de stock. C'est la couche qui doit survivre à un changement de framework, de base de données ou de canal d'exposition.
- Infrastructure — dialogue avec le monde extérieur : accès à la base de données, clients HTTP, brokers de messages, systèmes de fichiers, e-mail. Elle implémente des interfaces déclarées par la couche domaine ou application.
// Presentation: knows HTTP, knows nothing about pricing rules
@RestController
@RequestMapping("/orders")
class OrderController {
private final PlaceOrder placeOrder;
OrderController(PlaceOrder placeOrder) { this.placeOrder = placeOrder; }
@PostMapping
ResponseEntity<OrderResponse> create(@Valid @RequestBody OrderRequest body) {
OrderId id = placeOrder.handle(body.customerId(), body.lines());
return ResponseEntity.created(URI.create("/orders/" + id.value()))
.body(new OrderResponse(id.value()));
}
}
// Application: one use case, one transaction, no rules of its own
@Service
class PlaceOrder {
private final OrderRepository orders; // interface owned by the domain
private final PricingPolicy pricing; // domain service
PlaceOrder(OrderRepository orders, PricingPolicy pricing) {
this.orders = orders; this.pricing = pricing;
}
@Transactional
OrderId handle(CustomerId customer, List<OrderLine> lines) {
Order order = Order.draft(customer, lines); // domain enforces invariants
order.price(pricing); // domain rule, not controller code
orders.save(order);
return order.id();
}
}
// Infrastructure: implements the domain's interface with JPA
@Repository
class JpaOrderRepository implements OrderRepository {
// mapping between Order and the persistence entity lives here, not in the domain
}
Le détail important, c'est le sens de la flèche. PlaceOrder dépend de OrderRepository, une interface déclarée à côté de Order dans le package du domaine. JpaOrderRepository, dans le package d'infrastructure, l'implémente. Le domaine n'importe donc ni JPA, ni Spring, ni quoi que ce soit d'autre que du Java pur, et vous pouvez tester unitairement Order et PricingPolicy sans base de données.
Découpage strict ou relâché
Dans un découpage strict, chaque couche ne peut appeler que celle située immédiatement en dessous. Dans un découpage relâché, une couche peut appeler n'importe quelle couche inférieure, si bien qu'un contrôleur peut attaquer directement un repository pour une lecture triviale. Le découpage strict produit des méthodes passe-plats qui ne font que déléguer ; le découpage relâché incite les contrôleurs à accumuler de la logique métier. Un compromis défendable : la présentation n'appelle que l'application ; l'application appelle le domaine et les interfaces d'infrastructure ; personne n'appelle la présentation.
Ports et adaptateurs : la même idée, dessinée autrement
L'architecture hexagonale, dite aussi ports et adaptateurs, ainsi que les variantes « clean » et « onion » qui l'ont suivie, conservent les mêmes responsabilités mais changent le schéma. Au lieu d'une pile avec la base de données tout en bas, les couches domaine et application occupent le centre, et tout ce qui dialogue avec le monde extérieur — HTTP, base de données, broker, interface utilisateur — est un adaptateur branché sur un port (une interface) à la périphérie du cœur. La base de données n'est plus « sous » le domaine ; c'est un adaptateur parmi d'autres, sur le même plan que le contrôleur web.
Pourquoi ce nouveau dessin compte-t-il ? Parce que dans la pile traditionnelle, la couche de persistance est tout en bas et tout semble en dépendre ; les équipes finissent donc par concevoir le domaine autour des tables. Placer le domaine au centre rend la règle de dépendance explicite : les dépendances du code source pointent vers l'intérieur, vers le domaine, jamais vers l'extérieur. L'interface OrderRepository ci-dessus est un port ; JpaOrderRepository est un adaptateur. Si vous respectez déjà cette règle, vous avez déjà une architecture hexagonale, quelle que soit l'allure du diagramme.
Les données qui franchissent les frontières entre couches
Chaque frontière soulève la même question : quels objets la traversent ?
- Entre présentation et application : utilisez des objets de requête et de réponse propres à l'API (les records conviennent très bien en Java 21). N'exposez jamais directement les entités de persistance ; le contrat d'API et le schéma des tables changent pour des raisons différentes, et sérialiser des entités fait remonter dans les réponses des relations chargées en lazy et des champs internes.
- Entre application et domaine : passez des objets du domaine. Un mapping à cet endroit est généralement du gaspillage.
- Entre domaine et infrastructure : l'interface du repository parle en termes du domaine (
Order,OrderId) et l'adaptateur fait la conversion vers ce dont le stockage a besoin.
La règle empirique : un DTO (data transfer object) mérite sa place quand les deux côtés évoluent indépendamment ou franchissent le réseau ; à l'intérieur d'un même processus, entre deux couches qui changent toujours ensemble, ce n'est qu'une formalité.
Erreurs courantes
De la logique métier dans le contrôleur. Un contrôleur qui vérifie le stock, calcule une remise et décide s'il faut prévenir l'entrepôt ne peut être réutilisé ni depuis un batch ni depuis un listener de messages, et ne se teste pas sans HTTP. Correctif : le contrôleur traduit et délègue ; chaque if qui touche au métier part dans le domaine.
Domaine anémique, service obèse. Des classes du domaine réduites à des getters et des setters, pendant qu'une classe *Service concentre toutes les règles. Ces règles finissent dupliquées d'un service à l'autre et impossibles à garantir comme invariants. Correctif : placez le comportement sur les objets qui possèdent les données — c'est order.addLine(...) qui valide, pas OrderService.validateLine(...).
Des entités qui fuient jusqu'à l'API. Renvoyer des entités JPA depuis un endpoint REST couple le format d'échange au schéma, provoque des exceptions de lazy-initialisation et expose des champs que vous n'aviez jamais eu l'intention de publier. Correctif : des records de réponse dédiés et un mapper explicite.
Découpage par technologie, packages par couche uniquement. Une base de code dont les packages de premier niveau sont controllers, services et repositories, avec des centaines de classes dans chacun, est difficile à parcourir et permet à n'importe quel service d'appeler n'importe quel repository. Correctif : découpez d'abord par fonctionnalité (orders, billing), puis par couche à l'intérieur de la fonctionnalité, et rendez les éléments internes de la fonctionnalité package-private.
Des tiers ajoutés par effet de mode. Un « tier d'intégration » séparé pour un système qui n'a qu'un seul partenaire externe ajoute un saut réseau et un déploiement pour rien. Correctif : n'ajoutez un tier que si vous savez nommer la propriété d'isolation, de montée en charge ou de sécurité qu'il apporte.
Des préoccupations transverses réimplémentées dans chaque couche. Journalisation, authentification et gestion des transactions recopiées dans chaque classe. Correctif : traitez-les une seule fois — filtres ou intercepteurs en périphérie, transactions déclaratives dans la couche application, aspects là où c'est pertinent.
Bonnes pratiques
- Rédigez la matrice des flux avant le premier diagramme de déploiement ; elle oblige à se poser les questions de sécurité et de traçabilité.
- Faites respecter la règle de dépendance de façon automatique. Des tests à la ArchUnit qui cassent le build quand
domainimporteinfrastructuredemandent une heure de mise en place et évitent des années de dérive. - Gardez la couche domaine exempte d'annotations de framework autant que possible. Si une règle a besoin de
@Transactionalpour être correcte, elle est dans la mauvaise couche. - Faites de la couche application la frontière transactionnelle : un cas d'usage, une transaction, un seul endroit où regarder quand quelque chose n'est commité qu'à moitié.
- Traitez un nouveau tier comme une décision produit, avec un responsable, un SLA et un runbook, pas comme une boîte sur une slide.
- Quand une fonctionnalité traverse de nombreuses couches pour faire quelque chose de trivial, remettez en question la rigueur du découpage plutôt que d'ajouter des méthodes passe-plats.
Points clés à retenir
- Les tiers sont des frontières de déploiement ; les couches sont des frontières dans le code. Changez l'un sans supposer que l'autre doit suivre.
- Le deux-tiers plaçait la logique dans le client, le trois-tiers l'a déplacée vers un serveur d'applications, le n-tiers a scindé le milieu pour l'isolation et la montée en charge — chaque étape a échangé de la simplicité contre une propriété que vous devez être capable de nommer.
- Décrivez un système à quatre niveaux — fonctionnel, applicatif, technique, logiciel — et servez-vous d'une matrice des flux pour relier les applications à l'infrastructure.
- À l'intérieur d'un composant, les dépendances pointent vers l'intérieur, vers le domaine ; c'est le domaine qui déclare les interfaces que l'infrastructure implémente.
- L'architecture hexagonale, c'est la même règle avec le domaine dessiné au centre, ce qui empêche les équipes de tout concevoir autour de la base de données.
- Les anti-patterns coûteux sont la logique dans les contrôleurs, les domaines anémiques et les entités exposées sur le réseau ; tous trois sont peu coûteux à prévenir tôt et chers à défaire plus tard.
Sources et lectures complémentaires
Questions fréquentes
- Une architecture trois-tiers est-elle la même chose qu'une architecture en trois couches ?
- Non. Les tiers sont physiques : des processus ou des machines distincts, comme un navigateur, un serveur d'applications et un serveur de base de données. Les couches sont logiques : des ensembles de code aux responsabilités distinctes, qui peuvent tous tourner dans un seul processus. Un déploiement trois-tiers contient généralement plus de trois couches logiques, et une application desktop mono-tier peut très bien être proprement découpée en couches.
- Chaque couche doit-elle avoir ses propres classes de données (DTO) ?
- Prévoyez une représentation distincte partout où la forme ou le cycle de vie diffèrent. Les objets de requête et de réponse de l'API doivent être séparés des entités de persistance, car ils évoluent pour des raisons différentes et exposent des champs différents. Entre la couche application et la couche domaine, passer directement les objets du domaine convient en général ; y ajouter une étape de mapping est une formalité qui n'apporte rien.
- L'architecture en couches est-elle dépassée face aux microservices ?
- Elles répondent à des questions différentes. Les couches organisent le code à l'intérieur d'une unité déployable ; les microservices découpent un système en plusieurs unités déployables. Tout microservice bien construit est découpé en couches en interne, et la plupart des migrations réussies vers les microservices partent d'un monolithe dont les couches étaient assez propres pour être découpées. Un mauvais découpage en couches rend les deux styles pénibles.
- Une couche basse peut-elle appeler une couche haute ?
- Pas directement. Les dépendances doivent aller dans un seul sens, en général de l'extérieur vers l'intérieur. Quand une couche basse doit déclencher un comportement dans une couche haute, elle publie un événement ou appelle une interface qui lui appartient et que la couche haute implémente. Cette inversion préserve le cœur de toute référence aux frameworks et aux canaux d'exposition.