Structure des Packages
Nos services backend suivent un modèle Package par Fonctionnalité organisé en quatre modules frères. Ce document est la source de vérité pour la localisation du nouveau code.
Note linguistique. Les exemples ci-dessous sont rédigés en Java/Spring Boot car c'est notre stack backend principal, mais la structure elle-même est agnostique au langage et obligatoire pour chaque langage backend que nous utilisons (Kotlin, Go, Python, Node, etc.). Traduisez les extensions de fichiers et les annotations de framework ; conservez l'agencement des dossiers, les suffixes de nommage (
*Db,*Repo,*RepoImpl,*Dao,*Controller,*ControllerImpl) et la séparation interface/implémentation inchangés.
Principe fondamental
Grouper par fonctionnalité, pas par couche technique. Une modification de la fonctionnalité "commandes" doit concerner des fichiers qui se trouvent les uns à côté des autres, et non des fichiers dispersés dans les packages controllers/, services/, repositories/. Le regroupement par couche technique se fait à la frontière des modules (db, client, capabilities, starter), et le package par fonctionnalité se produit à l'intérieur de chaque module.
Les quatre modules
Chaque service est composé de ces quatre modules frères :
<service>/
├── capabilities/ # fonctionnalité/logique métier — le cerveau
├── db/ # persistance : entités, repositories, migrations
├── client/ # intégrations sortantes avec les systèmes externes
└── starter/ # Lanceur Spring Boot — assemble tout et exécute
capabilities/ — logique métier
Chaque fonctionnalité a son propre package, et à l'intérieur de chaque fonctionnalité, il y a un sous-package par point d'API (getById, create, updateById, getByQuery, …). Chaque sous-fonctionnalité possède trois dossiers frères :
service/— la classe de service unique pour cette APIdto/—request/etresponse/(selon le cas)mapper/— traduit les entités*Db↔ DTOs (dans les deux sens) pour cette seule sous-fonctionnalité
Le contrôleur de la fonctionnalité se trouve à la racine du package de la fonctionnalité, divisé en une interface et une implémentation comme pour les repositories :
capabilities/<feature>/<Feature>Controller.java— interface ; déclare les routes, les verbes HTTP, les types de requête/réponse, les annotations de documentation APIcapabilities/<feature>/<Feature>ControllerImpl.java— implémentation ; porte l'annotation du framework (@RestController) et délègue chaque route au service de sous-fonctionnalité correspondant
Partagé par toutes les sous-fonctionnalités de cette fonctionnalité. Voir Contrôleurs.
- Dépend de :
db(uniquement via les interfaces*Repo— voir Règle d'utilisation de db),client - Ne dépend jamais de :
starter - Ne contient pas de classe
main, ni de@SpringBootApplication - Possède : contrôleurs HTTP, DTOs de requête/réponse d'API, mappers, services
db/ — persistance
Organisé un dossier par table. Chaque dossier de table a exactement trois sous-dossiers :
db/<tableName>/
├── model/
│ ├── <EntityName>Db.java # entité JPA principale
│ └── <Other>Db.java # sous-modèles (types intégrés, entités enfants, projections)
├── dao/
│ └── <EntityName>Dao.java # accès aux données bas niveau (Criteria/JPQL/requêtes natives)
└── repository/
├── <EntityName>Repo.java # INTERFACE publique — le seul type visible par capabilities
└── impl/
└── <EntityName>RepoImpl.java # compose Spring Data + Dao derrière l'interface
Convention de nommage
| Couche | Suffixe | Exemple |
|---|---|---|
| Entité JPA | Db | TestcaseDb |
| Interface du repository | Repo | TestcaseRepo |
| Implémentation du repository | RepoImpl | TestcaseRepoImpl |
| Assistant d'accès aux données | Dao | TestcaseDao |
Règle d'utilisation de db
Capabilities doivent dépendre uniquement des interfaces *Repo. *RepoImpl et *Dao sont internes — jamais injectés ou référencés depuis l'extérieur du package db/<tableName>/. C'est ce qui permet de rendre la couche de persistance interchangeable et de concentrer les tests de capabilities sur le contrat.
- Dépend de : rien dans ce service
- Expose : les interfaces
*Repoet les types de modèles*Db - Ne contient jamais : de logique métier, de transformation en DTOs d'API
client/ — intégrations sortantes
Contient le code qui appelle les autres services dont ce service dépend. Principalement un sous-dossier par microservice dépendant de notre propre plateforme (paymentService, userService, executionService, …). Les wrappers de SDK tiers ou les producteurs de messages peuvent aussi se trouver ici, mais le modèle dominant — et la raison d'être de ce module — est celui des clients de microservices dépendants.
client/<microserviceName>/
├── <Microservice>Client.java # interface — ce que capabilities utilise
├── impl/
│ └── <Microservice>ClientImpl.java # impl : appel HTTP/gRPC/SDK, tentatives, timeouts
├── dto/ # formes de requête/réponse pour l'API DE CE microservice
│ ├── request/
│ └── response/
└── mapper/ # facultatif : traduit entre leurs formes et les nôtres
Règles
-
Un sous-dossier par microservice dépendant, nommé d'après le service. Ne regroupez pas plusieurs microservices dans un même package même s'ils sont "liés".
-
Capabilities dépendent uniquement des interfaces
*Client.*ClientImplest interne — même règle que pour*Repo/*RepoImpl. -
L'interface client expose l'API du microservice en termes de ses propres DTOs ; les capabilities peuvent les adapter si nécessaire.
-
Les préoccupations transversales (tentatives, circuit breaking, journalisation requête/réponse, en-têtes d'authentification) appartiennent à
impl/— les capabilities ne doivent jamais les voir. -
Dépend de : rien dans ce service
-
Expose : les interfaces
*Clientet leurs DTOs de requête/réponse -
Ne contient jamais : de logique métier, d'orchestration de capabilities
starter/ — racine de composition
Le seul module qui s'exécute. Contient @SpringBootApplication, les gestionnaires d'exception REST globaux, la configuration de sécurité, les filtres, et application.yml. Pas de contrôleurs — ils vivent dans capabilities/.
- Dépend de :
capabilities(et transitivementdb,client) - Possède : l'artefact déployable, la configuration web transversale (sécurité, CORS, gestion des erreurs)
- Ne possède jamais : de contrôleurs de fonctionnalité ou de DTOs de fonctionnalité
Règle de dépendance
starter → capabilities → { db, client }
Les flèches vont dans un seul sens. db et client n'importent jamais depuis capabilities ou starter. capabilities n'importe jamais depuis starter.
Contrôleurs
Il y a un contrôleur par fonctionnalité, pas par sous-fonctionnalité, et il se trouve à la racine du package de la fonctionnalité dans capabilities/. Il est divisé en deux fichiers, reflétant la séparation *Repo / *RepoImpl :
capabilities/<feature>/<Feature>Controller.java— interface. Déclare les routes, les verbes HTTP, les paramètres de chemin/requête, les types de DTOs de requête/réponse et les annotations de documentation API (OpenAPI/Swagger). Quiconque lit l'interface devrait voir l'intégralité du contrat HTTP.capabilities/<feature>/<Feature>ControllerImpl.java— implémentation. Porte l'annotation du framework (@RestControllerdans Spring), est la cible d'injection pour les services de sous-fonctionnalité, et contient une méthode courte par route qui appelle leservice/correspondant et retourne le résultat. Pas de logique au-delà de la délégation.
Pourquoi la séparation ? L'interface est le contrat API — stable, documenté, testable isolément. L'implémentation est le câblage — elle peut changer sans modifier le contrat. Même raisonnement que *Repo vs *RepoImpl.
Le contrôleur délègue chaque route HTTP au service de sous-fonctionnalité correspondant dans capabilities/<feature>/<sub-feature>/service/. Les DTOs de requête/réponse appartiennent à la sous-fonctionnalité (capabilities/<feature>/<sub-feature>/dto/) ; le contrôleur n'est qu'un adaptateur HTTP léger.
Pourquoi dans capabilities/ et pas dans starter/ ? Le contrôleur, son service et ses DTOs changent tous ensemble lorsqu'une API change — les garder dans un même module préserve la promesse du package par fonctionnalité : "une fonctionnalité, un endroit". starter/ est réservé au câblage transversal (sécurité, gestionnaires d'erreurs, point d'entrée de l'application), pas au code web par fonctionnalité.
Exemple concret : une fonctionnalité testcase
L'arborescence ci-dessous utilise des extensions .java pour l'illustration. Remplacez l'extension de fichier et les idiomes de framework de votre langage — Kotlin .kt, packages Go, modules Python, etc. — mais conservez l'agencement des dossiers et les suffixes de nommage identiques.
testcase-service/
├── capabilities/
│ └── com/example/testcase/capabilities/
│ └── testcase/ # fonctionnalité principale
│ ├── TestcaseController.java # interface — contrat HTTP, déclarations de routes, docs API
│ ├── TestcaseControllerImpl.java # impl — @RestController, délègue aux services de sous-fonctionnalités
│ ├── getById/ # sous-fonctionnalité = une API
│ │ ├── service/
│ │ │ └── GetTestcaseByIdService.java
│ │ ├── dto/
│ │ │ └── response/
│ │ │ └── GetTestcaseByIdResponse.java
│ │ └── mapper/
│ │ └── GetTestcaseByIdMapper.java # TestcaseDb → réponse
│ ├── create/
│ │ ├── service/
│ │ │ └── CreateTestcaseService.java
│ │ ├── dto/
│ │ │ ├── request/
│ │ │ │ └── CreateTestcaseRequest.java
│ │ │ └── response/
│ │ │ └── CreateTestcaseResponse.java
│ │ └── mapper/
│ │ └── CreateTestcaseMapper.java # requête → TestcaseDb, TestcaseDb → réponse
│ ├── updateById/
│ │ ├── service/
│ │ │ └── UpdateTestcaseByIdService.java
│ │ ├── dto/
│ │ │ ├── request/
│ │ │ │ └── UpdateTestcaseByIdRequest.java
│ │ │ └── response/
│ │ │ └── UpdateTestcaseByIdResponse.java
│ │ └── mapper/
│ │ └── UpdateTestcaseByIdMapper.java
│ └── getByQuery/
│ ├── service/
│ │ └── GetTestcaseByQueryService.java
│ ├── dto/
│ │ ├── request/
│ │ │ └── GetTestcaseByQueryRequest.java
│ │ └── response/
│ │ └── GetTestcaseByQueryResponse.java
│ └── mapper/
│ └── GetTestcaseByQueryMapper.java
│
├── db/ # un dossier par table
│ └── com/example/testcase/db/
│ └── testcase/ # nom de la table
│ ├── model/
│ │ ├── TestcaseDb.java # entité JPA — principale
│ │ └── TestcaseStepDb.java # sous-modèle (enfant/intégré)
│ ├── dao/
│ │ └── TestcaseDao.java # assistant de requête/criteria, utilisation privée au package
│ └── repository/
│ ├── TestcaseRepo.java # interface — capabilities dépendent de CECI
│ └── impl/
│ └── TestcaseRepoImpl.java # compose Spring Data + TestcaseDao
│
├── client/ # un dossier par microservice dépendant
│ └── com/example/testcase/client/
│ ├── executionService/ # microservice dépendant que nous appelons
│ │ ├── ExecutionServiceClient.java # interface
│ │ ├── impl/
│ │ │ └── ExecutionServiceClientImpl.java # appel HTTP/gRPC, tentatives, timeouts
│ │ ├── dto/
│ │ │ ├── request/
│ │ │ │ └── RunTestcaseRequest.java
│ │ │ └── response/
│ │ │ └── RunTestcaseResponse.java
│ │ └── mapper/
│ │ └── ExecutionServiceMapper.java
│ └── userService/ # un autre microservice dépendant
│ ├── UserServiceClient.java
│ └── impl/
│ └── UserServiceClientImpl.java
│
└── starter/ # câblage transversal uniquement — pas de contrôleurs
└── com/example/testcase/
├── TestcaseServiceApplication.java
├── config/
│ ├── SecurityConfig.java
│ └── WebConfig.java
├── error/
│ └── GlobalExceptionHandler.java # @ControllerAdvice — transversal, pas par fonctionnalité
└── resources/application.yml
Règles empiriques
- Ajouter une nouvelle API à une fonctionnalité existante ? Créez un nouveau package de sous-fonctionnalité dans
capabilities/<feature>/avec ses propresservice/,dto/{request,response}/, etmapper/, puis ajoutez la déclaration de route dans<Feature>Controller.java(interface) et la méthode de délégation dans<Feature>ControllerImpl.java. Ne créez pas un second contrôleur. - Ajouter une toute nouvelle fonctionnalité ? Créez le package de fonctionnalité dans
capabilities/(contrôleur à la racine, sous-fonctionnalités par API), et le dossier de table correspondant dansdb/<tableName>/avecmodel/,dao/,repository/{impl/}. Ajoutez un client uniquement si vous appelez un système externe. Ne touchez pas àstarter/. - Injectez toujours
*Repo, jamais*RepoImplou*Dao. Les capabilities dépendent uniquement de l'interface.*RepoImplet*Daosont internes àdb/<tableName>/— ils existent pour vous permettre de changer la stratégie de requête ou le stockage sous-jacent sans modifier le code de capability. - Injectez toujours
*Client, jamais*ClientImpl. Même règle pour les appels aux microservices dépendants. Les capabilities dépendent de l'interface ; le transport (HTTP, gRPC, SDK) vit derrièreimpl/. - Un dossier par microservice dépendant dans
client/. Nommez le dossier d'après le microservice. Ne regroupez pas "tous les services internes" dans un seul package. - La granularité des sous-fonctionnalités s'arrête à
capabilities/.db/est organisé par table ;client/par système externe. Diviser l'un ou l'autre par API disperserait un code cohérent. - Les DTOs de requête/réponse et leur mapper sont spécifiques à chaque sous-fonctionnalité, jamais partagés. Si
createetupdateByIdse ressemblent aujourd'hui, gardez deux DTOs et deux mappers. Le partage couple deux API qui dériveront indépendamment. - Les mappers convertissent entité ↔ DTO directement. Pas de couche de modèle de domaine intermédiaire.
<SubFeature>Mapperconvertit*Db↔ les DTOs de requête/réponse de cette sous-fonctionnalité. - Trois sous-fonctionnalités ont un code similaire ? Résistez à la tentation d'extraire un service ou un mapper partagé tant que vous n'avez pas le même modèle dans quatre endroits ou plus.
- Réutilisation entre fonctionnalités ? Placez les utilitaires partagés dans un package
common/à l'intérieur decapabilities/.