Layered, ou la couche qui en sait tropLayered, or the layer that knows too much
Présentation, Métier, Données : le découpage le plus ancien et le plus utile. Sa valeur n'est pas dans les trois boîtes, mais dans une règle de savoir — chaque couche ne connaît que sa préoccupation. Quand une remise se calcule dans la vue et se recalcule dans le SQL, le découpage ne sépare plus rien : c'est la couche anémique, l'anti-pattern qui ressemble à du code propre.Presentation, Business, Data: the oldest and most useful split. Its value isn't in the three boxes but in a rule of knowledge — each layer knows only its concern. When a discount is computed in the view and recomputed in the SQL, the split separates nothing: that's the anemic layer, the anti-pattern that looks like clean code.
AudienceAudience
Dev qui range son code en couches et veut savoir où elles fuientDev who files code into layers and wants to know where they leak
Trois couches, une seule question : chacune sait-elle seulement son métier ?Three layers, one question: does each one know only its own job?
C'est la première séparation que tout le monde dessine : Présentation, Métier, Données. Le découpage le plus utile et le plus ancien — et il marche, à une condition rarement dite à voix haute. La valeur n'est pas dans les trois boîtes ; elle est dans une règle de savoir : chaque couche ne connaît que sa préoccupation et ne parle qu'à celle d'en dessous. Le jour où une couche en sait trop, le découpage ne sépare plus rien.It's the first separation everyone draws: Presentation, Business, Data. The oldest, most useful split — and it works, on one condition rarely said out loud. The value isn't in the three boxes; it's in a rule of knowledge: each layer knows only its concern and talks only to the one below. The day a layer knows too much, the split separates nothing.
Chaque couche répond à une seule question. La Présentation ne décide pas ; les Données ne raisonnent pas. Le découpage tient tant que la connaissance ne déborde pas d'un étage à l'autre.Each layer answers one question. Presentation doesn't decide; Data doesn't reason. The split holds as long as knowledge doesn't spill from one floor to another.
Le réflexe du volumeThe volume's reflex
« Cette couche connaît-elle des choses qu'elle ne devrait pas connaître ? » Le Vol 1 réglait le sens des dépendances ; ce volume règle la répartition du savoir. Une remise calculée dans la vue, une règle métier cachée dans une requête SQL : la flèche peut être au bon sens et la connaissance, au mauvais étage.'Does this layer know things it shouldn't know?' Vol 1 settled the direction of dependencies; this volume settles the distribution of knowledge. A discount computed in the view, a business rule buried in a SQL query: the arrow can point the right way while the knowledge sits on the wrong floor.
Couches ≠ flèches.Layers ≠ arrows.
Ne pas confondre avec le Vol 1 #2. Là, la frontière disait dans quel sens pointent les dépendances. Ici, le découpage en couches dit qui a le droit de savoir quoi. Les deux axes sont orthogonaux : un code peut être bien stratifié et mal orienté, ou inversé proprement mais avec une couche métier vide. On traite ici le second axe.Don't conflate with Vol 1 #2. There, the boundary said which way dependencies point. Here, the layered split says who is allowed to know what. The two axes are orthogonal: code can be well-stratified yet wrongly oriented, or cleanly inverted yet with an empty business layer. We handle the second axis here.
№ 02La douleurThe pain4 min
La couche métier se vide, et la règle se réfugie partout ailleurs.The business layer empties out, and the rule takes refuge everywhere else.
Le découpage en couches ne tombe pas en panne bruyamment — il se vide. Une règle simple — « 10 % de remise pour un client gold au-dessus de 100 € » — doit vivre quelque part. Sous la pression du sprint, elle se pose là où on code à l'instant : un if dans le contrôleur, un CASE dans le SQL, une condition dans le template. La couche métier, censée la porter, ne contient plus que des appels qui passent le plat.Layering doesn't fail loudly — it hollows out. A simple rule — '10% off for a gold customer above €100' — must live somewhere. Under sprint pressure it lands wherever you're coding right now: an if in the controller, a CASE in the SQL, a condition in the template. The business layer, meant to hold it, ends up with nothing but calls passing the plate.
№ 1 · La règle se dupliqueThe rule duplicates
La remise est recalculée dans le contrôleur web, dans l'export CSV et dans la requête de reporting. Trois copies qui divergent dès qu'on change le seuil — et personne ne sait laquelle fait foi.The discount is recomputed in the web controller, the CSV export and the reporting query. Three copies that drift the moment the threshold changes — and no one knows which is canonical.
№ 2 · On ne sait plus où chercherYou can't find the rule
« Où s'applique la remise ? » n'a plus de réponse unique. Dans le service ? Le trigger SQL ? Le front ? Le coût n'est pas la performance : c'est le temps qu'il faut pour répondre à une question métier simple.'Where does the discount apply?' no longer has a single answer. In the service? The SQL trigger? The frontend? The cost isn't performance: it's the time it takes to answer a simple business question.
№ 3 · Le domaine ne modélise rienThe domain models nothing
Les entités sont des sacs de getters/setters ; toute la logique vit dans des « services » qui les manipulent de l'extérieur. C'est le modèle de domaine anémique : la couche métier existe sur le schéma, pas dans le code.Entities are bags of getters/setters; all logic lives in 'services' that manipulate them from outside. That's the anemic domain model: the business layer exists on the diagram, not in the code.
La règle qui a fui dans deux couchesThe rule that leaked into two layers
# Présentation — le contrôleur décideif user.tier == "gold" and cart.total > 100: # ✗ règle métier ici
cart.total *= 0.9# Données — le SQL re-décide la même chose, autrement
SELECT total * CASE WHEN tier='gold' AND total>100 THEN 0.9 ELSE 1 END # ✗
Le diagnostic.The diagnosis.
Le détecteur, ici, n'est pas le test mais la question : « si la règle change, combien de fichiers, dans combien de couches, dois-je rouvrir ? » Si la réponse est « plus d'un », la connaissance a débordé. Le réflexe : ramener chaque décision dans la couche dont c'est le métier, et n'y laisser que ça.The detector here isn't the test but the question: 'if the rule changes, how many files, across how many layers, must I reopen?' If the answer is 'more than one', knowledge has overflowed. The reflex: pull each decision back into the layer whose job it is, and leave only that there.
№ 03Le rapatriementThe repatriation5 min
On suit une règle, et on la regarde rentrer chez elle.Follow one rule, and watch it come home.
Stratifier ne consiste pas à dessiner trois boîtes : c'est à décider, pour chaque savoir, dans quelle boîte il a le droit de vivre. La remise « gold au-dessus de 100 € » est une décision métier — donc elle appartient à la couche métier, et nulle part ailleurs. L'animation suit ce seul morceau de connaissance pendant qu'on le retire de la vue et du SQL pour le concentrer là où c'est sa place.Layering isn't drawing three boxes: it's deciding, for each piece of knowledge, which box it's allowed to live in. The 'gold above €100' discount is a business decision — so it belongs in the business layer, and nowhere else. The animation follows that one piece of knowledge as we pull it out of the view and the SQL to concentrate it where it belongs.
Animation — où vit la décision ?Animation — where does the decision live?
Départ — la règle a fuiStart — the rule leaked · 1/4
PrésentationPresentation
comment on montrehow we show
remise
│
MétierBusiness
(passe le plat)(passes the plate)
│
DonnéesData
où l'état vitwhere state lives
remise
La remise est décidée dans la vue ET dans le SQL. La couche métier, elle, ne contient qu'un appel qui passe le plat.The discount is decided in the view AND in the SQL. The business layer holds only a call passing the plate.
La décision concentrée dans sa coucheThe decision concentrated in its layer
# Métier — le SEUL endroit qui décide d'une remiseclass Cart:
def total_due(customer) -> Money:
if customer.is_gold and self.subtotal > 100:
return self.subtotal * 0.9return self.subtotal
# Présentation : affiche total_due(), ne (re)calcule rien# Données : persiste le sous-total brut, ne (re)décide rien
Le motif transférable.The transferable pattern.
« Une décision n'a qu'un seul domicile, et c'est la couche dont c'est le métier. » Vrai pour la remise, vrai pour la validation, vrai pour le calcul de taxe. La même discipline donne MVC, MVVM, MVP côté interface : trois manières de garder la vue bête et la décision ailleurs. Stratifier, c'est répondre une fois pour toutes à « qui a le droit de savoir ça ? ».'A decision has only one home, and it's the layer whose job it is.' True for the discount, true for validation, true for tax calculation. The same discipline yields MVC, MVVM, MVP on the UI side: three ways to keep the view dumb and the decision elsewhere. Layering is answering, once and for all, 'who is allowed to know this?'
Modèle mentalMental model
L'imageThe image
Une entreprise avec ses services. La compta ne décide pas de la stratégie commerciale, l'accueil ne fixe pas les prix, l'atelier ne signe pas les contrats. Chaque service ne connaît que son affaire — et une décision donnée se prend à un seul bureau, celui dont c'est le mandat.A company with its departments. Accounting doesn't set commercial strategy, the front desk doesn't set prices, the workshop doesn't sign contracts. Each department knows only its own business — and a given decision is made at one desk, the one whose mandate it is.
La règleThe rule
Chaque couche ne connaît que sa préoccupation ; une décision (remise, validation, taxe) a un seul domicile — la couche dont c'est le métier. Les autres l'appellent, elles ne la refont pas.Each layer knows only its concern; a decision (discount, validation, tax) has a single home — the layer whose job it is. The others call it, they don't redo it.
Où l'analogie casseWhere the analogy breaks
Trois couches sur un schéma ne prouvent rien. On peut avoir Controller / Service / Repository parfaitement dessinés et une couche métier qui ne décide rien — le domaine anémique : la règle a fui dans la vue et le SQL pendant que l'organigramme, lui, restait impeccable. La couche n'est pas le dossier ; c'est la réponse à « qui a le droit de savoir ça ? ». On peut respecter les dossiers et violer la règle.Three layers on a diagram prove nothing. You can have Controller / Service / Repository perfectly drawn and a business layer that decides nothing — the anemic domain: the rule leaked into the view and the SQL while the org chart stayed impeccable. The layer isn't the folder; it's the answer to 'who is allowed to know this?'. You can honor the folders and still break the rule.
№ 04Le piègeThe trap4 min
Trois couches bien dessinées — et une couche métier qui ne pense pas.Three neatly drawn layers — and a business layer that doesn't think.
Le piège n'est pas d'oublier une couche : c'est d'avoir une couche métier qui ne porte aucune décision. Les entités deviennent des sacs de getters/setters, et toute la logique part dans des « services » qui les manipulent de l'extérieur. C'est le modèle de domaine anémique (Fowler) — un anti-pattern d'autant plus dangereux qu'il ressemble à du code propre. Bascule le toggle : regarde où vit le comportement.The trap isn't forgetting a layer: it's having a business layer that carries no decision. Entities become bags of getters/setters, and all logic moves into 'services' that manipulate them from outside. That's the anemic domain model (Fowler) — an anti-pattern all the more dangerous for looking like clean code. Flip the toggle: watch where behavior lives.
# Modèle riche — le comportement vit AVEC la donnée qu'il gardeclass Cart:
def add(self, item):
if self.is_locked: raise CartLocked() # l'invariant est protégé ici
self._items.append(item)
def total_due(self, customer) -> Money:
base = sum(i.price for i in self._items)
return base * 0.9if customer.is_gold and base > 100else base
# le service orchestre, il ne décide pas :class Checkout:
def run(cart, customer): pay(cart.total_due(customer)); cart.lock()
Pourquoi c'est justeWhy it's right
Le comportement est collé à la donnée qu'il garde : on ne peut pas ajouter à un panier verrouillé, parce que add() est le seul chemin et qu'il porte la garde. La remise a un domicile unique : total_due(). Le service Checkout orchestre — il appelle, il ne recalcule pas. La règle change ? Un fichier, une couche, un seul endroit.Behavior is glued to the data it guards: you can't add to a locked cart, because add() is the only path and it carries the guard. The discount has a single home: total_due(). The Checkout service orchestrates — it calls, it doesn't recompute. The rule changes? One file, one layer, one place.
La règle qui tranche.The rule that settles it.
Le comportement appartient à la donnée qu'il protège. Si pour connaître une règle il faut lire un service plutôt que l'entité qu'elle gouverne, le savoir a fui de sa couche. Un « service » légitime orchestre plusieurs entités ; un service illégitime fait ce qu'une entité aurait dû faire toute seule. La question : cette logique a-t-elle besoin de quelque chose hors de l'entité ? Sinon, elle vit dans l'entité.Behavior belongs to the data it protects. If knowing a rule means reading a service rather than the entity it governs, knowledge has leaked out of its layer. A legitimate 'service' orchestrates several entities; an illegitimate one does what an entity should have done alone. The question: does this logic need anything outside the entity? If not, it lives in the entity.
Anémique n'est pas toujours un bug.Anemic isn't always a bug.
Le contre-poids honnête : si le domaine n'a aucune règle — un CRUD qui range des champs en base — alors une entité riche est une cérémonie vide, et un modèle anémique + transaction scripts est le bon choix. L'anémie n'est un anti-pattern que là où il y avait un métier à porter. On y revient au bilan.The honest counterweight: if the domain has no rules — CRUD that files fields into a table — then a rich entity is empty ceremony, and an anemic model + transaction scripts is the right call. Anemia is only an anti-pattern where there was a business to carry. More in the wrap-up.
№ 05Bilan & éditoWrap-up & editorial2 min
La règle éparpillée sur trois étages, ou concentrée dans sa couche.The rule scattered across three floors, or concentrated in its layer.
Les deux découpages, côte à côteThe two splits, side by side
Couches qui fuientLeaky layers
Savoir bien rangéKnowledge in place
Où vit une règleWhere a rule lives
vue + service + SQLview + service + SQL
une seule couchea single layer
Changer la règleChange the rule
rouvrir 3 couches, espérerreopen 3 layers, hope
un fichier, un endroitone file, one place
Les entitésThe entities
sacs de getters/settersbags of getters/setters
gardent leurs invariantsguard their invariants
« Où est la logique ? »'Where's the logic?'
sans réponse uniqueno single answer
dans la couche dont c'est le métierin the layer whose job it is
Idée cléKey idea
la connaissance débordeknowledge overflows
chaque couche ne sait que son jobeach layer knows only its job
Quand ne pas enrichir le domaineWhen not to enrich the domain
Le découpage en couches a un loyer modeste ; le domaine riche, lui, peut être un sur-coût net. On le laisse anémique — et c'est correct — quand :Layering's rent is modest; a rich domain, though, can be a net overcost. Leave it anemic — and that's fine — when:
le domaine n'a pas de règles : un CRUD d'administration qui crée, lit, met à jour des lignes sans invariant. Une entité riche n'y protège rien — c'est de la cérémonie.the domain has no rules: an admin CRUD that creates, reads, updates rows with no invariant. A rich entity protects nothing there — it's ceremony.
la logique est intrinsèquement transverse : un calcul qui croise dix entités n'appartient à aucune. Là, un service (un transaction script) est le bon foyer — ce n'est pas une fuite, c'est sa couche.the logic is inherently cross-cutting: a calculation spanning ten entities belongs to none. There a service (a transaction script) is the right home — it's not a leak, it's its layer.
le découpage horizontal en couches commence lui-même à coûter : quand chaque feature traverse les trois couches et qu'on les rouvre ensemble à chaque fois, le découpage vertical par feature (Vertical Slices) répartit mieux le savoir. On le verra au Vol 2 #2.the horizontal split itself starts to cost: when every feature crosses all three layers and you reopen them together each time, a vertical, per-feature split (Vertical Slices) distributes knowledge better. We'll see it in Vol 2 #2.
Le réflexe n'est donc pas « domaine riche partout ». C'est : « cette règle a-t-elle un foyer naturel — une entité qu'elle gouverne ? Si oui, elle y vit, et les autres couches ne la connaissent pas ; sinon, elle vit dans un service, et c'est sa place, pas une fuite. »So the reflex isn't 'rich domain everywhere'. It's: 'does this rule have a natural home — an entity it governs? If yes, it lives there, and the other layers don't know it; if not, it lives in a service, and that's its place, not a leak.'
Mot de l'éditeurFrom the editor
Le découpage en couches est si banal qu'on oublie ce qu'il promet : non pas trois dossiers, mais une réponse à « qui a le droit de savoir ça ? ». Le débutant range ses fichiers par type — controllers/, services/, models/ — et croit avoir architecturé. L'architecte range le savoir par responsabilité, et vérifie qu'aucune couche n'en sait plus que son métier.Layering is so banal we forget what it promises: not three folders, but an answer to 'who is allowed to know this?'. The beginner files code by type — controllers/, services/, models/ — and thinks they've architected. The architect files knowledge by responsibility, and checks that no layer knows more than its job.
Le Vol 1 réglait le sens des flèches ; ce numéro règle la répartition du savoir — deux axes orthogonaux d'une même phrase. La suite généralise la règle de dépendance vers l'intérieur (Clean, Hexagonal) et lui oppose son contre-courant, le découpage vertical par feature. Mais la question de fond ne bougera plus : chaque morceau de code ne devrait connaître que ce dont il a la charge.Vol 1 settled the direction of arrows; this issue settles the distribution of knowledge — two orthogonal axes of the same sentence. What follows generalizes the inward dependency rule (Clean, Hexagonal) and sets it against its counter-current, the vertical per-feature split. But the underlying question won't move: each piece of code should know only what it's responsible for.
Les voisins que ce numéro débloqueThe neighbors this issue unlocks
MVC · MVVM · MVP — la même règle de savoir appliquée à l'interface : garder la vue passive et la décision ailleurs (contrôleur, présentateur, view-model). Trois variantes d'un seul réflexe.the same rule of knowledge applied to the UI: keep the view passive and the decision elsewhere (controller, presenter, view-model). Three variants of one reflex.
Transaction Script vs Domain Model — les deux foyers possibles de la logique métier : une procédure par cas d'usage, ou un comportement porté par les entités. Le choix dépend de la richesse des règles, pas de la mode.the two possible homes for business logic: one procedure per use case, or behavior carried by entities. The choice depends on how rich the rules are, not on fashion.
Anemic Domain Model — l'anti-pattern nommé par Fowler : des entités sans comportement et des services qui les pilotent de l'extérieur. Légitime en CRUD pur, piège dès qu'il y a un invariant à tenir : vider une entité de ses règles ne les supprime pas, ça les éparpille dans les services — le piège de la relocalisation du Vol 0 #4.Fowler's named anti-pattern: behavior-free entities driven from outside by services. Legitimate in pure CRUD, a trap the moment there's an invariant to hold: emptying an entity of its rules doesn't remove them, it scatters them across the services — the relocation trap of Vol 0 #4.
Le kata — écris-le, ne le relis pasThe kata — write it, don't reread it
Tu as suivi la remise rentrer chez elle au chapitre 3. Reste le geste qui ancre : l'écrire. Copie ce fichier de tests dans ton langage habituel — la spec tient dans les noms des tests — et fais-le passer au vert sans revenir au numéro. Deux tests portent la subtilité : celui qui interdit toute porte dérobée vers l'état (add() est le seul chemin, la garde ne vit pas dans un service qu'on contourne), et le dernier — la règle a un seul domicile, l'export l'appelle au lieu de la recopier.You watched the discount come home in chapter 3. What's left is the gesture that anchors it: writing it. Copy this test file into your usual language — the spec fits in the test names — and turn it green without coming back to the issue. Two tests carry the subtlety: the one that forbids any back door to state (add() is the only path, the guard doesn't live in a bypassable service), and the last one — the rule has a single home, the export calls it instead of recopying it.
test_cart_rules.py
# test_cart_rules.py — la spec EST le harnais : fais-le passer au vert.# une entité qui garde ses invariants et loge la règle en UN endroit. sans framework.def test_total_applique_la_remise_gold():
cart = Cart(items=[Item(60), Item(60)]) # sous-total 120
assert cart.total_due(gold) == 108# -10 % au-dessus de 100
assert cart.total_due(regular) == 120# personne d'autre n'y a droitdef test_pas_de_remise_sous_le_seuil():
cart = Cart(items=[Item(90)])
assert cart.total_due(gold) == 90# 90 < 100 : riendef test_panier_verrouille_refuse_l_ajout():
cart = Cart(items=[Item(10)]); cart.lock()
with raises(CartLocked): cart.add(Item(5)) # l'invariant est tenu par l'entitédef test_l_ajout_est_le_seul_chemin_vers_l_etat():
cart = Cart(items=[])
assert not hasattr(cart, "items") # pas de porte dérobée vers l'état# on ne peut pas contourner add() : la garde ne vit pas dans un service externedef test_la_regle_a_un_seul_domicile():
cart = Cart(items=[Item(200)])
# l'export réutilise total_due, il ne la recopie pas.
assert export_total(cart, gold) == cart.total_due(gold) # une seule source de vérité
Chez toi, cette semaine.In your codebase, this week.
Choisis une règle métier à valeur — un seuil, un taux de remise, un plafond — et cherche ses copies : grep -rn "0\.9\|is_gold\|gold" src/. Si la même constante vit dans la vue ET dans une requête ET dans un service, la règle n'a pas de domicile ; elle mentira au prochain changement. Ramène-la dans l'entité qu'elle gouverne, et fais appeler les autres au lieu de recalculer. La question du bilan : « cette règle a-t-elle un foyer naturel ? »Pick one valued business rule — a threshold, a discount rate, a cap — and hunt its copies: grep -rn "0\.9\|is_gold\|gold" src/. If the same constant lives in the view AND in a query AND in a service, the rule has no home; it will lie at the next change. Pull it back into the entity it governs, and make the others call instead of recompute. The wrap-up's question: 'does this rule have a natural home?'