Separation of Concerns : pourquoi votre code finit toujours par tout mélanger (et comment s'en sortir)
Le fichier qui gère à la fois la requête HTTP, la validation, l'accès base de données, l'envoi d'email et le log d'erreur existe dans tous les projets. La Separation of Concerns est le principe qui permet de ne pas en arriver là. Voici comment l'appliquer sans tomber dans l'excès inverse.
Un jour ou l'autre, dans tout projet qui vit assez longtemps, un fichier devient le cauchemar de l'équipe. Il fait quatre cents, six cents, parfois mille lignes. Il contient une route HTTP, la validation des paramètres, des requêtes SQL écrites à la main, du calcul métier, un envoi d'email, un appel à une API externe et deux blocs de log. Personne ne veut y toucher parce que chaque modification casse quelque chose d'inattendu.
Ce fichier est la démonstration vivante d'un principe qui n'a pas été respecté : la Separation of Concerns. Contrairement à SOLID, DRY, KISS ou YAGNI, il est rarement enseigné comme un principe à part. Pourtant, il sous-tend presque tout ce qu'on appelle "architecture" dans un projet logiciel.
Le problème concret que la Separation of Concerns cherche à résoudre
Partons d'un cas que tout développeur backend a vu au moins une fois. Une route d'inscription utilisateur écrite rapidement, parce que "c'est juste pour avancer" :
app.post('/register', async (req, res) => {
const { email, password, name } = req.body;
if (!email || !email.includes('@')) return res.status(400).send('email invalide');
if (!password || password.length < 8) return res.status(400).send('mot de passe trop court');
const existing = await db.query('SELECT id FROM users WHERE email = ?', [email]);
if (existing.length > 0) return res.status(409).send('email déjà utilisé');
const hash = await bcrypt.hash(password, 10);
const result = await db.query('INSERT INTO users (email, password, name) VALUES (?, ?, ?)', [email, hash, name]);
await mailer.send(email, 'Bienvenue', `Bonjour ${name}, votre compte est créé.`);
console.log('user created', result.insertId);
res.status(201).json({ id: result.insertId });
});
Ce code fonctionne. Pour une démo ou un tout petit projet, il peut même suffire. Mais il mélange cinq préoccupations différentes dans la même fonction :
- La gestion du protocole HTTP (lecture du body, codes de retour).
- La validation des données d'entrée.
- L'accès à la base de données (SQL écrit en dur).
- La logique métier (hashage, règles d'unicité).
- L'envoi de notifications (email).
Le jour où il faut ajouter une inscription via OAuth, changer de base de données, remplacer le service d'email ou tester la logique sans déclencher de vrai envoi, toute cette fonction doit être réécrite. Et si le projet en contient cinquante du même genre, le coût devient rédhibitoire.
La Separation of Concerns, formalisée par Edsger Dijkstra dans les années 1970, exprime une idée simple : chaque portion de code doit s'occuper d'une seule "préoccupation" à la fois. Préoccupation signifie ici une dimension du problème : l'interaction avec le monde extérieur, la logique métier, le stockage, la présentation, la configuration. Les mélanger dans un même endroit revient à fabriquer sa propre dette technique.
"La seule façon de maîtriser un grand programme est de le diviser en parties suffisamment petites pour être comprises séparément." Edsger Dijkstra, 1974
Refactoriser avec la Separation of Concerns
Reprenons la route d'inscription. Voici une version où chaque préoccupation est isolée dans sa propre couche :
// Couche validation
function validateRegistration(input) {
if (!input.email || !input.email.includes('@')) throw new ValidationError('email invalide');
if (!input.password || input.password.length < 8) throw new ValidationError('mot de passe trop court');
}
// Couche accès aux données
class UserRepository {
async findByEmail(email) { /* ... */ }
async create(user) { /* ... */ }
}
// Couche métier
class RegisterUser {
constructor(userRepo, hasher, notifier) {
this.userRepo = userRepo;
this.hasher = hasher;
this.notifier = notifier;
}
async execute(input) {
validateRegistration(input);
if (await this.userRepo.findByEmail(input.email)) throw new ConflictError('email déjà utilisé');
const hash = await this.hasher.hash(input.password);
const user = await this.userRepo.create({ ...input, password: hash });
await this.notifier.sendWelcome(user);
return user;
}
}
// Couche HTTP
app.post('/register', async (req, res, next) => {
try {
const user = await registerUser.execute(req.body);
res.status(201).json({ id: user.id });
} catch (err) {
next(err);
}
});
À première vue, on passe de vingt lignes à quarante. Certains diront que c'est de la sur-ingénierie. En réalité, chaque responsabilité peut maintenant être remplacée, testée ou réutilisée indépendamment. La classe RegisterUser ne connait ni Express, ni MySQL, ni SendGrid. Elle orchestre la logique métier et rien d'autre.
Si un jour on passe à PostgreSQL, seule l'implémentation de UserRepository change. Si on remplace Express par Fastify, seule la couche HTTP bouge. Si on veut tester la règle métier sans connexion réelle, on passe des mocks aux dépendances. C'est exactement le bénéfice que visent les principes d'inversion de dépendance décrits dans l'article SOLID.
Les trois échelles de Separation of Concerns
Ce principe ne s'applique pas qu'au niveau du fichier. Il se décline à plusieurs échelles, et c'est ce qui le rend si structurant.
Au niveau du code : fonctions et classes
C'est l'échelle la plus visible. Une fonction fait une chose, une classe regroupe des comportements cohérents autour d'une même responsabilité. C'est ici que rejoignent le principe SRP de SOLID et celui de KISS : si une fonction a besoin du mot "et" dans sa description, elle fait trop de choses.
Au niveau du module : séparation par couche
Les applications web suivent souvent une architecture en couches : présentation (routes, templates), application (cas d'usage), domaine (règles métier pures), infrastructure (base de données, services externes). Chaque couche a sa propre responsabilité et ses propres règles de dépendance. Typiquement, la couche métier ne dépend de rien d'extérieur, pendant que l'infrastructure s'adapte au métier et pas l'inverse.
C'est le raisonnement qui a donné naissance à des architectures comme Clean Architecture, Hexagonal (ports et adaptateurs) ou Onion. Elles sont toutes des formalisations différentes du même principe de fond.
Au niveau du système : services et domaines
À l'échelle d'un produit entier, la Separation of Concerns sépare les grands domaines fonctionnels. Authentification, facturation, recherche, notifications : chacun peut vivre dans son propre service, avec ses propres bases de données, son propre rythme de déploiement. C'est le terrain des microservices et du Domain Driven Design, qui ne sont au fond que ce principe appliqué à plus grande échelle.
La lecture du guide des technologies backend 2026 donne un bon panorama des outils qui outillent ces différentes échelles.
Frontend : la Separation of Concerns n'a pas disparu, elle a bougé
Pendant vingt ans, on a enseigné la Separation of Concerns au frontend sous une forme très concrète : le HTML pour la structure, le CSS pour le style, le JavaScript pour le comportement. Trois langages, trois fichiers, trois responsabilités.
L'arrivée des frameworks à composants, React en tête, a brouillé cette règle. Dans un composant React, du JSX, du style et de la logique cohabitent dans le même fichier. Certains ont crié à la trahison du principe. La réalité est plus nuancée : la séparation existe toujours, mais elle ne passe plus par le langage. Elle passe par la préoccupation fonctionnelle.
Un composant bien conçu sépare :
- La logique de présentation (composants purs, sans état ni appels API).
- La logique d'état (hooks personnalisés, stores).
- La logique métier (services, utilitaires).
- Les effets de bord (fetch, souscriptions, timers).
Le composant qui affiche une liste de produits ne devrait pas contenir la requête qui va les chercher. Un hook useProducts() ou un service dédié s'en charge. Le composant, lui, reçoit simplement des données prêtes à afficher. C'est ce découpage que les formations React et Next.js permettent d'adopter en pratique.
Les signaux qui révèlent une mauvaise séparation
Un code où la Separation of Concerns est défaillante émet des signaux reconnaissables. Si vous reconnaissez trois de ces situations dans votre projet, il est temps de faire un point.
Le test qui ne peut pas s'écrire sans démarrer l'application complète
Si tester une règle métier nécessite de lancer un serveur HTTP, une base de données et un client Redis, c'est que cette règle est collée à des préoccupations d'infrastructure. La règle en elle-même devrait être testable avec un simple appel de fonction.
Le changement de librairie qui impacte trente fichiers
Passer de axios à fetch ne devrait pas obliger à modifier la moitié du projet. Si c'est le cas, c'est que l'accès aux appels HTTP n'est pas encapsulé dans une couche dédiée. Chaque endroit qui utilise axios directement est un point de friction futur.
La requête SQL dans le contrôleur HTTP
C'est le symptôme classique. Un contrôleur ou une route qui contient du SQL en dur ou des appels ORM détaillés mélange deux couches. Le contrôleur ne devrait parler qu'au cas d'usage, qui parle au repository, qui parle à la base.
Le fichier de configuration qui contient de la logique
Un fichier de config qui commence à contenir des conditions complexes ou des transformations de données n'est plus une configuration. C'est du code déplacé à un endroit inadapté. La configuration déclare des valeurs. Si vous avez besoin de logique, elle appartient ailleurs.
L'équilibre : pas trop de couches non plus
Il existe une erreur symétrique : créer tellement de couches que comprendre un flux simple oblige à sauter dans dix fichiers différents. C'est l'écueil que YAGNI et KISS permettent justement de détecter. Un petit projet n'a pas besoin d'une architecture hexagonale complète. Une API qui expose trois endpoints peut se contenter d'un découpage en trois dossiers : routes, services, models.
La bonne séparation est celle qui correspond à la taille et aux besoins réels du projet. Ajouter des couches par anticipation d'une évolution hypothétique, c'est exactement ce contre quoi YAGNI met en garde. L'article YAGNI détaille bien ce piège.
Une heuristique pratique : commencez par un découpage minimal et introduisez une nouvelle couche quand la douleur devient concrète. Quand vous vous retrouvez à écrire la même glue dans plusieurs endroits, quand un test devient impossible à écrire, quand un changement ripple dans trop de fichiers : c'est le moment d'ajouter une séparation. Pas avant.
Le lien avec les autres principes de code
La Separation of Concerns n'est pas une règle isolée. Elle dialogue avec tous les autres principes fondamentaux et souvent les rend possibles.
Avec SOLID. Le principe de responsabilité unique (SRP) n'est rien d'autre qu'une formulation de la Separation of Concerns au niveau de la classe. L'inversion de dépendance (DIP) est l'outil technique qui permet de séparer effectivement les couches en les reliant par des abstractions plutôt que par des classes concrètes.
Avec DRY. On peut appliquer DRY naïvement et se retrouver avec une fonction utilitaire qui mélange dix préoccupations parce qu'elle est appelée de partout. Séparer les préoccupations d'abord, déduplifier ensuite, dans cet ordre : c'est ce que recommande l'article DRY.
Avec KISS. Une bonne séparation rend chaque morceau plus simple à comprendre individuellement. Mais trop de couches tuent KISS. L'équilibre est à trouver en permanence.
Avec YAGNI. Ne séparez que ce qui a besoin d'être séparé maintenant. La séparation anticipée de préoccupations qui n'existent pas encore est une forme de complexité accidentelle.
Comment progresser sur ce principe
La Separation of Concerns est un réflexe qui s'acquiert avec la pratique et surtout avec l'exposition à des codes bien structurés. Lire du code open source écrit par des équipes expérimentées apporte beaucoup. Les projets Symfony, Django, Spring ou NestJS sont des exemples où la séparation par couche est visible dès l'organisation des dossiers.
Travailler avec un framework qui impose une structure est aussi une bonne école. Symfony, par exemple, propose une séparation claire entre contrôleurs, services, entités et repositories. Les formations Symfony 7 et Spring Boot ont ces conventions intégrées dans leur ADN. Apprendre un de ces frameworks, c'est s'imprégner d'une manière de découper le code qu'on transporte ensuite dans d'autres contextes.
Pour ceux qui veulent pousser cette réflexion jusqu'à l'architecture d'application complète, la formation Fullstack React + Symfony montre comment la séparation frontend et backend se combine avec la séparation interne à chaque partie. C'est souvent à ce moment qu'un développeur saisit la cohérence globale du principe.
FAQ
La Separation of Concerns est-elle la même chose que le principe de responsabilité unique ?
Non, ce sont deux principes proches mais distincts. Le SRP s'applique au niveau d'une classe ou d'une fonction. La Separation of Concerns est plus large et s'applique à toutes les échelles : module, couche, service, système complet. Le SRP peut être vu comme l'application de la Separation of Concerns au niveau microscopique.
Dois-je adopter une architecture hexagonale dès le début d'un projet ?
Pas nécessairement. Une architecture hexagonale stricte apporte une vraie valeur sur des projets dont la durée de vie est longue et la logique métier complexe. Pour un MVP, un side-project ou une API simple, un découpage classique en couches suffit largement. Ajouter des ports et adaptateurs partout avant d'en avoir besoin complique le code sans bénéfice immédiat.
Le CSS dans les composants React viole-t-il la Separation of Concerns ?
Non, pas au sens moderne du principe. La séparation n'est plus par langage mais par préoccupation fonctionnelle. Un composant qui encapsule sa présentation, son comportement et son style pour une unité d'interface donnée respecte parfaitement le principe, tant qu'il ne mélange pas logique de présentation et logique métier ou accès aux données.
Comment savoir si j'ai trop de couches dans mon projet ?
Trois signaux concordants doivent alerter : la moindre fonctionnalité oblige à modifier plus de trois ou quatre fichiers, les nouveaux membres de l'équipe mettent des semaines à comprendre le flux d'une requête simple, et les couches contiennent souvent de la glue ou du passe-plat sans logique réelle. Si ces signes sont présents, vous êtes probablement dans la sur-ingénierie.
Les microservices sont-ils l'application logique de la Separation of Concerns ?
Oui, mais c'en est la forme la plus radicale et la plus coûteuse. Les microservices séparent physiquement les préoccupations. Le coût en devient réel : latence réseau, orchestration, cohérence des données, observabilité. Avant de découper un système en microservices, la plupart des bénéfices de la Separation of Concerns peuvent être obtenus par un bon découpage en modules au sein d'un même monolithe.
Ce principe s'applique-t-il aussi en développement frontend ?
Oui, pleinement. Séparer les composants de présentation des hooks de données, isoler la logique métier dans des services, regrouper les appels API dans une couche dédiée : tout cela relève de la Separation of Concerns appliquée au frontend. Un projet React bien structuré ressemble beaucoup, dans son esprit, à un projet backend bien organisé.
Quelle différence entre Separation of Concerns et modularité ?
La modularité est le moyen, la Separation of Concerns est le critère. Un code peut être modulaire sans bien séparer les préoccupations si chaque module mélange plusieurs responsabilités. À l'inverse, respecter la Separation of Concerns produit naturellement un code modulaire, parce que chaque module a alors une raison claire d'exister.
Peut-on appliquer ce principe à un projet existant déjà mal structuré ?
Oui, progressivement. La règle pragmatique est de ne pas tout réécrire d'un coup. Identifier les zones les plus douloureuses, extraire une préoccupation à la fois (par exemple sortir les accès base de données dans un repository dédié pour commencer), puis itérer. Chaque morceau isolé allège la charge cognitive sur le reste du projet et facilite les extractions suivantes.