Une application de gestion commence avec des dossiers Controller, Entity, Repository et Service. Trois ans plus tard, modifier une règle de retour touche un formulaire, deux listeners, une commande console et un import. Personne ne sait si le remboursement appartient à Commande, Paiement ou Support. Le problème n’est pas que Symfony manque de structure ; la structure technique initiale ne raconte plus les capacités du produit.
En pratique, l’objectif consiste à rendre le changement local. Une décision métier doit posséder un vocabulaire, un responsable, des données et des interfaces identifiables. Le projet peut conserver un seul dépôt et un seul déploiement tout en établissant des frontières fermes. La modularité commence dans les dépendances, pas dans le nombre de bundles ou de services réseau.
Contrairement à ce que suggère une arborescence propre, déplacer les classes ne suffit pas. Réorganiser les dossiers trop tôt peut masquer le travail difficile : un namespace Retour qui conserve les écritures croisées crée une façade de modularité. L’équipe doit partir des flux, des invariants et des conflits de responsabilité, puis déplacer le code à mesure que les contrats deviennent vrais et testés.
Dans un projet de développement web sur mesure, cette progression évite la grande réécriture. Elle sélectionne une douleur, ferme une frontière et mesure le résultat. Le plan suivant couvre Doctrine, services Symfony, échanges, tests et gouvernance jusqu’à un pilote exploitable.
Reconnaître un domaine qui déborde la structure
Suivre une modification récente
Le diagnostic choisit cinq demandes livrées et relève les classes, tables, files et équipes touchées. Si une règle commerciale exige des changements dispersés dans plusieurs contrôleurs, la décision n’a plus de lieu. Si un module central change pour toutes les demandes, il porte probablement trop de responsabilités ou un vocabulaire faussement commun.
Les incidents complètent l’analyse. Un statut inexplicable, une correction SQL ou une relance réservée à un expert signalent une frontière absente. Le nombre de fichiers n’est pas le problème ; l’impossibilité de prédire les effets d’une modification l’est. Le coût caché se trouve dans les revues, la recette élargie et le diagnostic.
Observer les dépendances plutôt que les noms
Un namespace « Domain » peut dépendre de Doctrine, du client HTTP et de la session. Un service appelé « Manager » peut modifier quatre agrégats. L’équipe inspecte les imports, les écritures et les effets déclenchés. Le nom ne vaut qu’après la preuve de la dépendance.
Cartographier les capacités avant les dossiers
Un atelier part des verbes du métier : tarifer, accepter une commande, préparer une livraison, facturer, rembourser. Chaque capacité possède une décision et un résultat. Les participants identifient les mots ambigus. « Client » peut désigner un compte commercial, un destinataire ou un payeur ; partager le même objet crée des dépendances inutiles.
La carte note les systèmes sources, les données possédées et les événements importants. Elle ne cherche pas à produire un modèle parfait. Elle révèle les endroits où deux équipes pensent posséder la même règle ou où aucune ne la possède. Ces conflits deviennent les candidats prioritaires.
Commencer par la frontière qui coûte
L’équipe choisit un module dont les changements fréquents provoquent des régressions ou du support. Un domaine stable mais imparfait peut attendre. La priorité combine fréquence, coût d’erreur et possibilité d’isoler un pilote. Cette décision évite un programme de rangement sans bénéfice métier.
Donner à chaque module une responsabilité
Un module contient ses cas d’usage, ses règles, ses ports et ses adaptateurs. Il expose des commandes et des requêtes nommées avec le vocabulaire métier. Les autres modules n’instancient pas ses entités et n’appellent pas ses repositories. Ils passent par un contrat public réduit.
La structure peut utiliser des namespaces comme CommandeApplication, CommandeDomain et CommandeInfrastructure. Elle n’est pas obligatoire pour chaque petite capacité. Une arborescence moins profonde reste acceptable si les dépendances vont dans le bon sens et si les responsabilités sont comprises.
Éviter le bundle comme unité métier automatique
Le système de bundles sert surtout à empaqueter et réutiliser des extensions. Un module interne n’a pas besoin de sa propre extension de conteneur. La documentation des bonnes pratiques Symfony déconseille de créer un bundle pour organiser le seul code applicatif. Namespaces, configuration et tests suffisent souvent.
Séparer décision, orchestration et infrastructure
Le domaine porte les invariants et calculs indépendants des mécanismes. L’application orchestre un cas d’usage, contrôle la transaction et appelle les ports. L’infrastructure implémente base, HTTP, messagerie ou fichier. Cette séparation permet de tester une règle sans kernel tout en conservant des tests d’intégration sur Doctrine et Symfony.
Elle ne doit pas multiplier les interfaces sans raison. Un port est utile lorsqu’il représente une dépendance externe, une source variable ou une frontière qu’il faut simuler. Une classe interne stable peut être injectée directement. La clarté prime sur la pureté.
Garder le contrôleur à sa place
Le contrôleur traduit identité, paramètres et format, puis appelle le cas d’usage. Il ne calcule pas le remboursement et ne choisit pas la prochaine transition. Une commande console et un consommateur peuvent réutiliser la même orchestration avec des entrées différentes.
Utiliser Doctrine sans laisser l’ORM dessiner le métier
Une entité Doctrine peut aussi porter des comportements, mais elle ne doit pas devenir le graphe global de l’application. Les relations bidirectionnelles entre modules rendent chaque chargement et chaque migration imprévisibles. Une référence scalaire ou un objet de valeur limite l’exposition.
Les repositories appartiennent au module qui possède l’agrégat. Un autre module demande une information par requête publique ou projection. Les lectures transverses peuvent être optimisées sans donner un droit d’écriture. Cette dissociation réduit les dépendances tout en acceptant que le monolithe partage encore une base.
Faire évoluer le schéma par étapes compatibles
Le code lit l’ancien et le nouveau format pendant la transition. Un backfill borné remplit les données, les écarts sont comptés, puis l’écriture bascule. Le rollback applicatif reste possible avant le retrait final. La migration est testée sur un volume représentatif.
Choisir des échanges synchrones ou événementiels
Une commande synchrone convient lorsqu’une décision est nécessaire pour répondre et que l’échec doit annuler l’opération. Un événement décrit un fait déjà accepté et permet des conséquences différées. Publier un événement pour éviter un appel ne supprime pas la dépendance ; il ajoute délai, livraison multiple et reprise.
Le contrat d’événement contient un identifiant, une version, la date et les données minimales. Les consommateurs tolèrent une nouvelle livraison. Une outbox relie la transaction métier à la publication lorsque la perte d’événement est inacceptable. La file et les erreurs sont observées comme une capacité du produit.
Nommer l’état intermédiaire
Si le remboursement est accepté mais pas encore exécuté, l’interface l’affiche. Elle ne confond pas la mise en file avec le résultat. Le support connaît les statuts, la corrélation et le geste de reprise. La modularité reste visible dans la promesse utilisateur.
Cas concret : reprendre un domaine de retours
Cas concret hypothétique. Un distributeur gère les retours dans Commande, Support et Paiement. Un agent approuve, un listener déclenche un avoir et un cron remet le stock. Une panne du prestataire de paiement laisse certains dossiers sans statut. Relancer le cron peut produire un second avoir.
La cible crée un module Retour. Il possède demande, décision, motifs et état de remboursement. Paiement reçoit une commande idempotente et renvoie un résultat ; Stock consomme un événement après acceptation physique. L’interface distingue retour accepté, remboursement en attente, remboursé et intervention requise.
Le pilote couvre cent retours, trois rôles et deux pannes simulées. Le seuil local exige zéro double avoir sur cinquante rejouages, cent pour cent des refus motivés et une reprise en moins de dix minutes sans accès SQL. Un état inconnu ou une écriture étrangère bloque l’extension. Ces nombres servent ce périmètre et seront révisés.
Mesurer la localisation du changement
Une nouvelle règle de délai doit toucher Retour et son contrat public, pas les contrôleurs de Support et de Paiement. L’équipe compare le nombre de modules modifiés, le temps de recette et les incidents. Le bénéfice vient de la réduction des effets imprévus.
Tester les règles et les frontières
Les tests unitaires exercent les invariants avec des objets simples. Les tests d’intégration vérifient mapping Doctrine, transactions et adaptateurs. Les tests de contrat fixent ce qu’un module accepte et renvoie. Quelques parcours de bout en bout prouvent l’assemblage sans porter toute la combinatoire.
Une règle d’architecture interdit les imports non autorisés. Elle est exécutée dans la CI. Les exceptions temporaires mentionnent motif, propriétaire et date de sortie. Une frontière non contrôlée se dégrade dès que la pression de livraison augmente.
Tester les refus comme des résultats
Un montant hors plafond, un rôle insuffisant ou un état périmé produit une erreur métier stable. Le contrôleur la traduit ; la règle ne dépend pas du format HTTP. Ces cas protègent la frontière mieux qu’un test qui vérifie seulement la réponse nominale.
Faire respecter l’architecture par l’outillage
Composer et l’autoloading reflètent les namespaces. La configuration des services peut limiter les ressources découvertes par l’autowiring, tandis que les services restent privés par défaut. Des outils d’analyse statique détectent dépendances interdites et types ambigus. Les commandes de CI restent rapides pour être exécutées à chaque changement.
La journalisation ajoute module, cas d’usage et corrélation. Le monitoring suit erreurs par capacité et messages en attente. Un tableau global sans propriété produit peu d’action ; une alerte Retour doit mener au runbook et à l’équipe responsable.
Documenter les décisions qui changent le code
Une décision courte explique pourquoi une frontière existe, quelles alternatives ont été refusées et quel signal déclencherait une révision. Elle ne remplace pas les tests. Elle aide les nouveaux arrivants à distinguer une contrainte d’une préférence.
Attribuer la responsabilité des modules
Chaque capacité possède un référent métier et un collectif technique, pas forcément une équipe exclusive. Ils valident contrats, changements sensibles et incidents. Le référent ne devient pas un goulot : la connaissance est partagée par revue, rotation et exercices de reprise.
La roadmap suit les dettes de frontière comme des risques produit. Une dépendance illégale peut être temporairement acceptée si son retrait est planifié. Sans échéance, l’exception devient la nouvelle architecture. Le responsable arbitre avec coût et bénéfice visibles.
Pour qui la modularisation doit rester légère
Une petite application stable n’a pas besoin de seize modules et de trois couches par fonctionnalité. Regrouper des capacités proches réduit la cérémonie. La séparation devient utile quand vocabulaire, fréquence de changement, données ou responsabilité divergent.
Un domaine en croissance bénéficie d’une modularisation progressive. Il n’a pas besoin de microservices immédiats. Si une capacité acquiert plus tard une charge ou une équipe autonome, ses contrats et sa propriété de données rendent l’extraction plus sûre.
Erreurs fréquentes quand le domaine grossit
Réorganiser tous les dossiers en une fois
La grande bascule produit un diff impossible à relire et mélange déplacement, comportement et migration. L’équipe ferme une frontière, garde les tests verts puis poursuit. Les anciens chemins peuvent cohabiter brièvement avec une échéance.
Créer une interface pour chaque classe
Une abstraction sans alternative ni frontière augmente le vocabulaire sans protéger le métier. Les interfaces se concentrent sur les ports, les contrats publics et les dépendances instables.
Partager un modèle canonique universel
Forcer tous les modules à utiliser le même Client, Produit ou Statut lie leurs évolutions. Les traductions locales paraissent redondantes, mais elles protègent des sens et des cycles distincts.
Arbitrer une nouvelle frontière
La matrice examine vocabulaire propre, invariants, données, fréquence de changement, incidents et responsabilité. Un candidat fort cumule plusieurs signaux. Une simple différence d’écran ne justifie pas un module séparé.
Le comité peut isoler maintenant, observer ou refuser. Il isole quand la dépendance coûte déjà et qu’un contrat est testable. Il observe quand les usages restent incertains. Il refuse lorsque la frontière ne possède ni décision ni données propres.
- D’abord, prioriser la capacité où les régressions et corrections manuelles consomment le plus de temps.
- Ensuite, commencer par interdire les nouvelles écritures croisées avant de déplacer tout le code.
- Puis, différer l’événement asynchrone tant que le délai et la reprise ne sont pas acceptés.
- Enfin, étendre lorsque le changement, le refus et l’incident restent locaux au module.
Plan d’action sur huit semaines
Semaines une et deux : établir la preuve
L’équipe suit changements et incidents, cartographie capacités et choisit une douleur. Elle fixe le résultat attendu, les seuils et le périmètre. Les responsabilités métier et techniques sont nommées.
Semaines trois et quatre : créer le contrat
Les entrées, sorties, refus et dépendances sont écrits. Les tests de règle et de contrat précèdent les déplacements. L’ancien code appelle progressivement la nouvelle orchestration. La CI bloque les nouvelles violations.
Semaines cinq et six : migrer données et flux
Les écritures rejoignent le module, les lectures transverses utilisent une requête publique et les messages nécessaires deviennent idempotents. La journalisation porte module et corrélation. Le monitoring relie chaque seuil à une action.
Semaines sept et huit : éprouver et décider
Un lot limité passe par le nouveau chemin. Le support exécute le runbook, un retry et le rollback. Le dossier final décrit contrat, dépendances, instrumentation, erreurs et dettes. L’équipe élargit seulement si les mesures locales tiennent sans aide orale.
Le pilote provoque aussi une version d’événement inconnue, un remboursement déjà exécuté et une référence de commande supprimée. Chaque cas doit produire un refus stable ou une reprise documentée. Le responsable métier confirme que le statut visible correspond à la décision, tandis que l’équipe technique retrouve la trace et la dépendance sans parcourir les autres modules.
La revue finale compare le délai d’une modification de règle, la surface de recette, les écritures croisées et le temps nécessaire pour expliquer un incident. Elle garde les mesures brutes et les limites de l’échantillon. Si le changement reste diffus, l’équipe corrige la frontière avant de déplacer une nouvelle capacité ; si le résultat tient, elle reprend la même méthode sur la douleur suivante.
La mise en œuvre attribue explicitement les responsabilités de la CI, du monitoring et du rollback. Elle nomme les entrées, les sorties, les dépendances et le seuil d’arrêt. Cette gouvernance empêche le module de devenir un simple dossier neuf autour d’un ancien couplage, tout en donnant à la maintenance un contrat vérifiable au prochain changement.
Une décision d’architecture courte consigne la frontière choisie, les solutions écartées et le signal qui déclencherait une révision. Produit et support la relisent avec un exemple de dossier, car un vocabulaire compris seulement par les développeurs ne localise pas la responsabilité. Cette trace aide une nouvelle personne à distinguer un invariant durable d’un compromis temporaire.
Le pilote inclut un contrôle de performance ciblé sur les nouvelles requêtes publiques. Une projection plus claire peut introduire une lecture coûteuse ou un chargement excessif ; l’équipe mesure plan SQL, volume et latence sur les données représentatives. Elle optimise le contrat sans redonner aux consommateurs un accès direct aux tables, afin que la pression du run ne dissolve pas la frontière obtenue.
La sortie de cette phase n’est pas une arborescence figée. C’est une capacité dont une nouvelle personne peut comprendre le vocabulaire, modifier une règle, observer un refus et reprendre un incident à partir des contrats publics. Ce critère protège l’adoption interne et révèle rapidement les abstractions qui n’aident que leurs auteurs.
Guides complémentaires pour prolonger le découpage
Maintenir un monolithe modulaire
Tenir un monolithe Symfony modulaire relie frontières, transactions et décision d’extraction.
Découper le domaine
Découper un domaine fonctionnel approfondit responsabilités, langage et contrats entre capacités.
Tester le workflow
Tester les exceptions d’un workflow aide à rendre les refus et reprises indépendants de l’interface.
- Partir d’une décision métier douloureuse plutôt que d’une arborescence idéale.
- Fermer les écritures et dépendances avant de multiplier les mécanismes d’échange.
- Vérifier chaque frontière par un changement, un refus et un incident simulé.
Conclusion : rendre le changement local
Un domaine qui grossit demande des frontières, pas une inflation de dossiers techniques. Les capacités métier fournissent une structure que produit, développement et support peuvent partager.
Doctrine, le conteneur et Messenger restent des moyens. Leurs contrats servent la décision ; ils ne doivent pas devenir le modèle du domaine.
La progression la plus sûre ferme une capacité à la fois et mesure la localisation du changement. Elle évite une réécriture et prépare une extraction seulement si une contrainte future la justifie.
Dawap peut vous accompagner pour cartographier le domaine, stabiliser une première frontière et installer ses contrôles dans un projet de développement web sur mesure conçu pour évoluer sans dépendances invisibles.