Vous ouvrez un nouveau projet et la première chose que vous faites — avant d'écrire une seule ligne de logique — c'est créer un dossier. Peut-être qu'il s'appelle controllers/. Peut-être qu'il s'appelle orders/. Dans les deux cas, ce choix tranquille est déjà une décision architecturale, et il façonnera silencieusement tout ce qui suit : la vitesse à laquelle un nouveau coéquipier prend ses marques, le nombre de fichiers que vous touchez quand une seule fonctionnalité change, et si dans six mois votre codebase se lit comme votre métier ou comme votre framework.
Cet article est un tour pratique des trois approches principales — par couche, par fonctionnalité et screaming (par domaine) — avec des compromis honnêtes à chaque taille d'entreprise. Aucune réponse unique ne gagne dans toutes les situations, mais vous repartirez avec un modèle mental clair pour choisir délibérément plutôt que par défaut.
Pourquoi la structure des dossiers est une décision architecturale, pas du pinaillage
Les gens balaient la structure des dossiers d'un revers de main en disant « c'est juste de l'organisation ». C'est bien plus que ça. La façon dont vous regroupez le code contrôle trois choses qui s'avèrent énormément importantes :
- Couplage. Quand du code qui change ensemble vit loin les uns des autres, chaque petite mise à jour devient une expédition entre dossiers. Quand il vit ensemble, les changements connexes se regroupent en un seul endroit.
- Découvrabilité. Un développeur qui ne connaît pas votre codebase va demander : « où est le code pour la facturation ? » La réponse à cette question — un dossier, ou cinq dossiers éparpillés — détermine le temps d'intégration.
- Rayon d'impact. Combien de fichiers devez-vous ouvrir pour livrer une nouvelle fonctionnalité, ou pour supprimer en toute sécurité une ancienne ? La structure contraint ce rayon d'impact ou le laisse se propager sans limites.
Pensez à votre structure de dossiers comme à une carte. Une bonne carte regroupe les choses par leur relation dans le monde réel — pas par le matériau dont elles sont faites. Regrouper les fichiers par type (controllers, models) revient à trier les icônes d'une carte par leur forme. Regrouper par fonctionnalité revient à trier par quartier. L'un aide à naviguer ; l'autre est techniquement correct mais pratiquement inutile.
La Loi de Conway — l'observation que les systèmes logiciels reflètent les structures de communication des équipes qui les construisent — s'applique ici aussi. Si votre équipe est organisée autour de capacités métier (une squad paiements, une squad commandes), une structure par fonctionnalité semblera naturelle et imposera les bonnes frontières. Une structure par couche se battra contre vous.
Par couche : le défaut familier
L'organisation par couche regroupe les fichiers par leur rôle technique. Un projet Node ou Spring Boot typique finit par ressembler à quelque chose comme ça :
src/
├─ controllers/
│ ├─ OrderController.ts
│ ├─ UserController.ts
│ └─ BillingController.ts
├─ services/
│ ├─ OrderService.ts
│ ├─ UserService.ts
│ └─ BillingService.ts
├─ repositories/
│ ├─ OrderRepository.ts
│ ├─ UserRepository.ts
│ └─ BillingRepository.ts
└─ models/
├─ Order.ts
├─ User.ts
└─ Invoice.ts
Tous les tutoriels de frameworks web aboutissent ici. Rails le fait, le starter Spring Boot le fait, les générateurs Express le font. Cette familiarité est son principal atout.
Là où la structure par couche brille :
- Les petites apps avec deux ou trois fonctionnalités — la structure est si plate qu'elle a peu d'importance.
- Les équipes où tout le monde travaille sur toutes les fonctionnalités ; la couche est l'unité de spécialisation.
- Les prototypes et les outils internes où la rapidité de mise en place initiale prime sur la clarté à long terme.
Là où la structure par couche fait mal :
- Une seule fonctionnalité est dispersée dans plusieurs dossiers. Ajouter une fonctionnalité « remboursement » signifie toucher
controllers/,services/,repositories/etmodels/. C'est quatre dossiers pour ce qui est conceptuellement une seule chose. - Supprimer une fonctionnalité est un projet d'archéologie. Comment savoir quels fichiers dans
services/appartiennent à la facturation versus aux commandes ? Le nom du dossier ne vous le dit pas. - Les conflits de fusion se multiplient. Chaque modification de fonctionnalité touche les mêmes fichiers au niveau des couches, provoquant des collisions quand deux développeurs travaillent en parallèle.
La structure par couche fonctionne bien jusqu'à environ 10–15 fonctionnalités. Au-delà, chaque dossier de premier niveau devient un tiroir à tout : des dossiers services/ avec 40 fichiers, aucune propriété claire, et des pull requests qui touchent des fichiers qu'un relecteur n'a jamais vus avant.
Par fonctionnalité : un changement vit dans un seul dossier
L'organisation par fonctionnalité regroupe les fichiers selon la tranche du produit qu'ils servent. Les couches techniques (controller, service, model) existent toujours — elles vivent juste à l'intérieur de chaque dossier de fonctionnalité au lieu d'être au premier niveau :
src/
├─ orders/
│ ├─ OrderController.ts
│ ├─ OrderService.ts
│ ├─ OrderRepository.ts
│ └─ Order.ts
├─ billing/
│ ├─ BillingController.ts
│ ├─ BillingService.ts
│ ├─ BillingRepository.ts
│ └─ Invoice.ts
├─ auth/
│ ├─ AuthController.ts
│ ├─ AuthService.ts
│ └─ User.ts
└─ shared/
├─ database.ts
├─ logger.ts
└─ Money.ts
Là où la structure par fonctionnalité brille :
- Un changement vit dans un seul dossier. Ajouter un flux de remboursement signifie ajouter des fichiers dans
billing/— aucun autre dossier n'est touché. Le rayon d'impact est petit et contenu. - Supprimer une fonctionnalité est simple. Supprimez le dossier. Si quelque chose en dehors importe encore depuis lui, votre IDE vous le dira immédiatement.
- La propriété est évidente. L'équipe paiements possède
billing/; l'équipe identité possèdeauth/. Les outils de monorepo et les règles de revue de code peuvent l'imposer mécaniquement. - L'intégration est plus rapide. Un nouveau développeur travaillant sur les commandes ne lit que
orders/. Il n'a pas besoin de comprendre tout le codebase d'abord.
Là où la structure par fonctionnalité nécessite de l'attention :
- Le placement du code partagé est gênant. Le type
Moneyest utilisé à la fois parorders/etbilling/. Doit-il vivre dansorders/? Ça semble bizarre. Il finit dansshared/— qui peut lui-même devenir un second tiroir à tout si personne ne le possède. - Les dépendances entre fonctionnalités nécessitent de la discipline. Si
orders/OrderService.tsimporte directement depuisbilling/BillingService.ts, vous avez créé un couplage que la structure des dossiers ne peut pas voir. Les équipes utilisent des règles de lint (eslint-plugin-import/no-restricted-paths) ou des outils de frontières de modules (Nx, modules NestJS) pour les rendre explicites.
Screaming Architecture : que dit votre premier niveau ?
Uncle Bob (Robert C. Martin) a inventé le terme Screaming Architecture dans un célèbre billet de blog : « Que crie l'architecture de votre application ? Quand vous regardez la structure du répertoire de premier niveau, et les fichiers sources dans le package de plus haut niveau ; crient-ils : Système de soins de santé, ou Système de comptabilité, ou Système de gestion des stocks ? Ou crient-ils : Rails, ou Spring/Hibernate, ou ASP.Net ? »
L'intuition est élégante : si vos dossiers de premier niveau sont controllers/, models/, views/, votre codebase crie framework MVC. Le framework est un outil — il ne devrait pas être la première chose qu'un lecteur rencontre.
La Screaming Architecture (aussi appelée par domaine) pousse la structure par fonctionnalité un cran plus loin. Non seulement les fonctionnalités sont au premier niveau — mais les noms de ces fonctionnalités sont tirés directement du langage métier (le Langage Ubiquitaire du Domain-Driven Design), et non des rôles techniques :
// Crie « plateforme e-commerce »
src/
├─ catalog/ // le concept de domaine : un catalogue de produits
├─ ordering/ // le concept de domaine : passer et suivre des commandes
├─ fulfillment/ // le concept de domaine : préparation, emballage, expédition
├─ payments/ // le concept de domaine : débits, remboursements, factures
└─ identity/ // le concept de domaine : comptes, auth, permissions
// Comparez avec l'alternative qui crie « framework » :
src/
├─ controllers/ // crie « MVC »
├─ models/ // crie « ORM »
├─ views/ // crie « moteur de templates »
└─ services/ // crie « pattern couche de service »
À l'intérieur de chaque dossier de domaine, la mise en page interne est laissée à l'équipe — et c'est bien. La promesse clé est que le premier niveau communique le métier, pas l'infrastructure. Un nouveau développeur (ou votre directeur technique, ou un expert métier du côté business) peut lire les dossiers de premier niveau et comprendre ce que fait le système sans savoir comment il est construit.
Comparaison côte à côte
Voici une évaluation honnête selon cinq dimensions pratiques :
| Dimension | Par couche | Par fonctionnalité | Par domaine (screaming) |
|---|---|---|---|
| Découvrabilité | Faible à grande échelle — « où est le code de facturation ? » n'a pas de réponse rapide | Élevée — un dossier par concept | Maximale — les noms de dossiers viennent du métier lui-même |
| Localité des changements | Mauvaise — un changement de fonctionnalité se disperse dans toutes les couches | Bonne — les changements se regroupent dans un dossier | Bonne — identique à par fonctionnalité, plus le langage de domaine explicite |
| Vitesse d'intégration | Lente — les nouveaux venus doivent comprendre toute la couche avant de toucher quoi que ce soit | Rapide — étudiez un dossier pour travailler sur une fonctionnalité | Maximale — les noms de domaine guident la compréhension sans contexte préalable |
| Risque de couplage | Élevé — les couches partagées deviennent des surfaces de couplage implicites | Moyen — les imports inter-fonctionnalités sont possibles mais visibles | Faible — contextes délimités explicites et frontières imposées |
| Risque de « Grande Boule de Boue » | Élevé — les couches deviennent des tiroirs à tout quand les fonctionnalités se multiplient | Moyen — le dossier shared/ peut croître sans limites | Faible — les frontières de domaine rendent la boue visible tôt |
Le hybride pragmatique sur lequel la plupart des bonnes équipes atterrissent
En pratique, les meilleures équipes ne choisissent pas une seule approche et ne l'appliquent pas partout avec une cohérence religieuse. Elles utilisent un hybride pragmatique : dossiers de fonctionnalité ou de domaine au premier niveau, petites couches techniques à l'intérieur de chacun.
Cet hybride vous offre le meilleur des deux mondes :
- Le premier niveau crie votre métier — les nouveaux venus savent immédiatement que c'est une app e-commerce, pas un squelette MVC générique.
- Chaque fonctionnalité est autonome — un changement dans le flux de facturation ne touche que
billing/. - Les couches techniques existent toujours à l'intérieur de chaque dossier, donc les développeurs qui pensent en termes de MVC ou de couches Clean Architecture ne sont pas perdus. Ils regardent juste à l'intérieur du dossier de fonctionnalité.
- Un dossier
shared/contient les préoccupations vraiment transverses (un objet-valeurMoney, un logger, une connexion de base de données). La règle clé : si quelque chose est ajouté àshared/, il doit être extrait et possédé délibérément — et non déversé là comme échappatoire.
Certaines équipes l'appellent shared/, d'autres common/, lib/ ou core/. Le nom compte moins que la discipline : traitez-le comme une mini bibliothèque interne. Si un fichier est régulièrement modifié parallèlement à une fonctionnalité spécifique, il n'appartient pas à shared — il appartient à l'intérieur de cette fonctionnalité.
À grande échelle : frontières imposées et monorepos
À mesure qu'un codebase grandit et que plusieurs équipes contribuent, les conventions de dossiers seules ne suffisent plus — un développeur peut toujours traverser une frontière avec un import relatif rapide. L'étape suivante consiste à rendre les frontières imposées :
- Les modules NestJS rendent les dépendances inter-modules explicites : vous n'avez accès qu'à ce qu'un autre module
exporte. Un import qui traverse une frontière de module sans passer par l'API publique est une erreur de compilation. - Les règles de frontière Nx vous permettent de déclarer quelles fonctionnalités ou bibliothèques sont autorisées à dépendre de quelles autres, puis de l'imposer comme erreur de lint en CI.
- Les Monorepos vont le plus loin : chaque domaine (orders, billing, auth) devient son propre package avec son propre
package.json, ses propres tests et ses exports publiés explicites. Turborepo et Nx sont les outils dominants dans l'écosystème JavaScript pour gérer ça. La structure ressemble moins à un arbre de dossiers et plus à un graphe de packages — mais l'idée sous-jacente est identique : regrouper par capacité métier, rendre les frontières explicites, les imposer mécaniquement.
Passer à un monorepo n'est pas toujours nécessaire. Beaucoup d'équipes fonctionnent heureusement avec un seul package et une structure hybride bien disciplinée pendant des années. Tournez-vous vers les outils de monorepo quand le couplage inter-équipes devient une vraie douleur — pas de façon préventive.
Recommandations par taille d'entreprise
La bonne structure dépend de l'endroit où votre organisation en est aujourd'hui. Voici une recommandation directe pour chaque stade :
| Stade | Structure à choisir | Pourquoi | Conseil du monde réel |
|---|---|---|---|
| Solo / startup pré-lancement | Par couche ou par fonctionnalité superficiellement | Vous avez 3–5 fonctionnalités. N'importe quelle structure fonctionne. Optimisez pour la vitesse et la simplicité. | Les défauts des tutoriels Rails/Express conviennent bien ici. Ne sur-ingénieriez pas. |
| Startup en croissance (5–20 ingénieurs) | Par fonctionnalité avec un dossier shared/ |
Les fonctionnalités se multiplient. La structure par couche commence à faire mal. Un changement dans les paiements ne devrait pas se propager dans les commandes. | De nombreuses entreprises Série A réussies vivent ici : un dépôt, un service, des fonctionnalités comme dossiers de premier niveau. |
| Scale-up (20–100 ingénieurs) | Hybride (domaine en haut, couches à l'intérieur) + règles de lint pour les frontières | Les équipes possèdent des domaines. Le couplage accidentel inter-domaines est un risque réel. Imposez les frontières en CI. | C'est le point idéal de Nx/modules NestJS. L'app Rails principale de Shopify a adopté une séparation forte des domaines à ce stade. |
| Enterprise (100+ ingénieurs) | Monorepo avec des packages de domaine, ou services indépendants par contexte délimité | L'isolation au niveau du package, le déploiement indépendant et les APIs publiques versionnées entre les domaines deviennent nécessaires. | Google, Meta et Airbnb gèrent des monorepos géants avec une propriété stricte. Beaucoup d'entreprises vont dans l'autre sens : des microservices par domaine, ce qui est la Screaming Architecture poussée à sa conclusion logique. |
L'erreur structurelle la plus courante n'est pas de choisir la mauvaise approche — c'est de rester trop longtemps sur la mauvaise approche. Les équipes démarrent par couche, ressentent la douleur à 20 fonctionnalités, et ne refactorisent toujours pas parce que « la structure fonctionne, techniquement ». Une réorganisation de dossiers d'un après-midi à 15 fonctionnalités évite des mois de confusion à 50.
Voir la différence : ce qui s'allume quand vous livrez une fonctionnalité
La façon la plus concrète de ressentir la différence entre par couche et par fonctionnalité est de demander : quelles boîtes s'allument quand j'ajoute une nouvelle fonctionnalité ?
billing/ s'allume. Le reste du codebase est structurellement isolé de ce changement.Ce diagramme est l'argument le plus clair pour le regroupement par fonctionnalité. Le « rayon d'impact » n'est pas un concept abstrait — c'est le nombre de dossiers et de fichiers que vous ouvrez dans une seule pull request. Les relecteurs, les pipelines CI et git blame fonctionnent tous mieux quand un changement de fonctionnalité est un bloc cohésif, pas une traînée à travers chaque couche.
Points clés à retenir
- La structure, c'est l'architecture. Votre organisation de dossiers contrôle le couplage, la découvrabilité et le rayon d'impact de chaque changement futur.
- Par couche est familier et convient aux petites apps, mais devient un tiroir à tout au-delà de 10–15 fonctionnalités.
- Par fonctionnalité garde les changements locaux. Une nouvelle fonctionnalité touche un seul dossier. Supprimer une fonctionnalité signifie supprimer un seul dossier.
- La Screaming Architecture dit que le premier niveau doit décrire votre métier, pas votre framework. Si une partie prenante non technique peut lire vos noms de dossiers et comprendre ce que fait le système, vous avez réussi.
- L'hybride pragmatique sur lequel la plupart des bonnes équipes atterrissent : noms de domaine/fonctionnalité au premier niveau, petites couches techniques à l'intérieur de chaque dossier.
- Imposez les frontières quand vous grandissez. Les conventions de dossiers sont des suggestions ; les systèmes de modules, les règles de lint et les packages de monorepo en font des garanties.
- Réorganisez tôt — un refactoring de dossiers d'un après-midi à 15 fonctionnalités évite des mois de confusion à 50.
Maintenant que vous avez vu comment structurer le code à l'intérieur d'un seul service, la question naturelle suivante est ce qu'il faut faire quand un seul service ne suffit plus. Continuez la lecture : Monolithe → Microservices.