Vol. 5 · № 01 · Un atelier d'architectureAn architecture workshop

Repository, ou le métier qui ignore sa baseRepository, or the domain that ignores its database

Une règle métier — « on n'expédie qu'une commande payée » — soudée à un SELECT, une colonne, un driver : le stockage a fui dans le raisonnement. Le Repository glisse une frontière : le métier parle à une collection feinte (find, add, save) qu'il possède, et une seule classe derrière sait que c'est une base. C'est l'inversion de dépendance du Vol 1 pointée sur la persistance. Mais un objet nommé « Repository » qui laisse fuir l'ORM ne fait que déguiser le couplage : la frontière n'existe que si rien du stockage ne la traverse.A business rule — 'you only ship a paid order' — welded to a SELECT, a column, a driver: storage has leaked into the reasoning. The Repository slides in a boundary: the domain talks to a feigned collection (find, add, save) it owns, and one class behind it knows that's a database. It's Vol 1's dependency inversion aimed at persistence. But an object named 'Repository' that leaks the ORM merely disguises the coupling: the boundary only exists if nothing from storage crosses it.

AudienceAudience
Dev dont les règles métier nomment encore des tables et des colonnesDev whose business rules still name tables and columns
Format
Self-paced
ChapitresChapters
5
Date
Juin 2026June 2026
≈ 18 min●○○○Données & étatRepositoryTradeoff
SommaireContents ·
№ 01CadrageFraming3 min

Une règle métier noyée dans du SQL. Qu'est-ce qui devrait varier ?A business rule drowned in SQL. What should be allowed to vary?

On ouvre le volume des données par sa pièce de base. ship() porte une vraie règle — on n'expédie qu'une commande payée — mais elle est entrelacée avec un SELECT, une colonne, un driver. Deux choses de natures opposées vivent dans la même fonction : la règle, stable et propre au métier ; le stockage, volatil et purement technique. Ce qui varie, ce n'est pas la règle : c'est où et comment les données sont rangées. Le réflexe : le métier devrait demander ses commandes comme à une collection en mémoire, sans jamais savoir qu'il y a une base derrière.We open the data volume with its foundational piece. ship() carries a real rule — you only ship a paid order — but it's interwoven with a SELECT, a column, a driver. Two things of opposite natures live in the same function: the rule, stable and domain-owned; the storage, volatile and purely technical. What varies isn't the rule: it's where and how the data is stored. The reflex: the domain should ask for its orders as if from an in-memory collection, never knowing there's a database behind.

Le code naïf — la règle soudée au stockageThe naive code — the rule welded to storage
class OrderService:
    def ship(order_id):
        row = db.query("SELECT * FROM orders WHERE id = %s", order_id)  # SQL ici
        if row["status"] != "paid":          # ← LA règle métier, noyée
            raise CannotShip("commande non payée")
        db.query("UPDATE orders SET status='shipped' WHERE id=%s", order_id)
        # la règle « on n'expédie qu'une commande payée » est vraie…
        # mais elle est soudée à la table, au driver, au SQL. Intestable sans base.
La règle vs le rangementThe rule vs the storage
« on n'expédie que payé »'only ship if paid'
la règle · stablethe rule · stable
+
soudée àwelded to
SELECT · UPDATE · driverSELECT · UPDATE · driver
le rangement · volatilthe storage · volatile

La règle ne change pas si l'on passe de Postgres à un autre stockage ; le SQL, si. Tant que les deux sont dans la même fonction, on ne peut tester la règle sans une vraie base, ni changer la base sans rouvrir le métier.The rule doesn't change if you move from Postgres to another store; the SQL does. As long as both sit in the same function, you can't test the rule without a real database, nor change the database without reopening the domain.

Le réflexe du volumeThe volume's reflex

« Est-ce que je lis et j'écris le même modèle — et est-ce que ça me coûte ? » Avant même cette question, il en faut une plus simple : le métier sait-il où vivent les données ? S'il connaît la table, il connaît tout le reste. Le Repository répond en glissant une frontière : le métier parle à une collection abstraite ; un seul endroit, derrière, sait que cette collection est une base. C'est la première marche du volume.'Do I read and write the same model — and does it cost me?' Before that question comes a simpler one: does the domain know where the data lives? If it knows the table, it knows everything else. The Repository answers by sliding in a boundary: the domain talks to an abstract collection; one place, behind it, knows that collection is a database. It's the volume's first step.

L'inversion de dépendance, sur les données.Dependency inversion, applied to data.

Le Repository n'est pas un pattern nouveau : c'est l'inversion de dépendance du Vol 1, pointée sur la persistance. Le métier possède l'interface (« je veux trouver, ajouter, enregistrer des commandes ») ; l'infrastructure l'implémente. C'est exactement un port piloté de l'hexagone (Vol 1 #2) : la flèche de dépendance va de la base vers le métier, jamais l'inverse. Tout ce numéro est une application concrète de ce déjà-vu.The Repository isn't a new pattern: it's Vol 1's dependency inversion, aimed at persistence. The domain owns the interface ('I want to find, add, save orders'); infrastructure implements it. It's exactly a driven port of the hexagon (Vol 1 #2): the dependency arrow runs from the database toward the domain, never the reverse. This whole issue is a concrete application of that déjà-vu.

№ 02La douleurThe pain4 min

Le schéma remonte partout, et la règle ne se teste qu'avec une base.The schema leaks everywhere, and the rule can only be tested with a database.

Mettre le SQL dans le métier a trois coûts qui se renforcent. Le schéma devient une dépendance publique : renommer une colonne casse des fonctions métier, parfois dans d'autres modules. La règle devient intestable sans infrastructure : pour vérifier « on n'expédie qu'une commande payée », il faut une vraie base, des fixtures, une connexion. Et la même requête se duplique : chaque service réécrit son SELECT, et le jour où la logique d'accès change, on la cherche partout.Putting SQL in the domain has three costs that reinforce each other. The schema becomes a public dependency: renaming a column breaks business functions, sometimes in other modules. The rule becomes untestable without infrastructure: to check 'you only ship a paid order', you need a real database, fixtures, a connection. And the same query duplicates: each service rewrites its SELECT, and the day the access logic changes, you hunt for it everywhere.

№ 1 · Le schéma fuit dans le métierThe schema leaks into the domain

Le nom de la table, des colonnes, le dialecte SQL deviennent connus de la logique métier. Renommer status en state, passer à un autre moteur, et c'est le code des règles qu'on rouvre — alors qu'aucune règle n'a changé.The table name, the columns, the SQL dialect become known to business logic. Rename status to state, switch engines, and it's the rules code you reopen — when no rule has changed.

№ 2 · La règle est intestable seuleThe rule is untestable alone

Tester « refuser d'expédier si non payé » exige une base réelle, des fixtures, une connexion qui rend les tests lents et fragiles. La règle la plus simple devient un test d'intégration, parce qu'on ne peut pas la séparer de son stockage.Testing 'refuse to ship if unpaid' requires a real database, fixtures, a connection that makes tests slow and brittle. The simplest rule becomes an integration test, because you can't separate it from its storage.

№ 3 · La requête se duplique et dériveThe query duplicates and drifts

« charger une commande avec ses lignes » est réécrit dans cinq services, chacun à sa façon. Une optimisation d'index, un filtre soft-delete oublié, et les copies divergent. L'accès aux données n'a pas de maison unique.'Load an order with its lines' is rewritten in five services, each its own way. An index tweak, a forgotten soft-delete filter, and the copies diverge. Data access has no single home.

Le test qui réclame une base pour une règle d'une ligneThe test that demands a database for a one-line rule
def test_refuse_ship_if_unpaid():
    db = spin_up_postgres()                 # 💥 une base entière…
    db.execute("INSERT INTO orders ...")     # … des fixtures SQL…
    with pytest.raises(CannotShip):          # … pour vérifier UN if.
        OrderService(db).ship(order_id)
Le diagnostic.The diagnosis.

Le détecteur : « une fonction qui porte une règle métier nomme-t-elle une table, une colonne ou écrit-elle une requête ? ». Si oui, le rangement a fui dans le raisonnement. Le réflexe : extraire un Repository — une interface qui parle le langage du métier (find, add, save d'entités), possédée par le métier, et une implémentation, à part, qui seule connaît le SQL. La règle redevient testable avec une collection en mémoire ; la base redevient un détail qu'on peut changer sans toucher au métier.The detector: 'does a function carrying a business rule name a table, a column, or write a query?'. If yes, the storage leaked into the reasoning. The reflex: extract a Repository — an interface that speaks the domain's language (find, add, save of entities), owned by the domain, and an implementation, apart, that alone knows the SQL. The rule becomes testable with an in-memory collection; the database becomes a detail you can change without touching the domain.

№ 03La collection feinteThe feigned collection5 min

On glisse une frontière : le métier parle à une collection, pas à une base.Slide in a boundary: the domain talks to a collection, not a database.

Le Repository fait croire au métier qu'il manipule une simple collection en mémoire — find, add, save — alors qu'un seul endroit, derrière, traduit ces gestes en SQL. L'animation installe cette frontière, puis inverse la flèche de dépendance comme au Vol 1 : ce n'est plus le métier qui dépend de la base, c'est la base qui réalise une interface possédée par le métier. Dernier temps : on débranche le SQL, on branche une collection en mémoire, et la règle se teste sans rien.The Repository makes the domain believe it's handling a plain in-memory collection — find, add, save — while one place, behind it, translates those gestures into SQL. The animation installs that boundary, then flips the dependency arrow as in Vol 1: it's no longer the domain depending on the database, it's the database realizing an interface owned by the domain. Final beat: unplug the SQL, plug an in-memory collection, and the rule tests with nothing.

Animation — la frontière entre la règle et le rangementAnimation — the boundary between rule and storage
Départ — le métier parle à la baseStart — the domain talks to the DB · 1/4
Métier
écrit du SQL · coupléwrites SQL · coupled
▶
SELECT / UPDATESELECT / UPDATE
Database
table · drivertable · driver
dépendance : métier → infrastructuredependency: domain → infrastructure
La flèche de dépendance va du métier vers l'infrastructure : il connaît la table, le SQL, le driver. Tout détail de stockage remonte dans le raisonnement.The dependency arrow runs from domain to infrastructure: it knows the table, the SQL, the driver. Every storage detail leaks up into the reasoning.

Le métier parle au contrat, l'infra le réaliseThe domain talks to the contract, infra realizes it
# le métier — possède l'interface, parle sa langue, ignore le SQL
interface OrderRepository: find(id) -> Order; save(order)
class ShipOrder(repo: OrderRepository):
    def run(order_id):
        order = repo.find(order_id)
        order.ship()            # la règle « payé ? » vit DANS l'entité, testable seule
        repo.save(order)

# l'infra — la seule à connaître la base
class SqlOrderRepository implements OrderRepository:
    def find(id): row = db.query("SELECT ..."); return to_order(row)
Le motif transférable.The transferable pattern.

« Le métier nomme ce qu'il veut, l'infra sait comment l'obtenir. » La même frontière, selon ce qu'elle cache, s'appelle Data Mapper (traduire entre l'entité et la table sans que l'une connaisse l'autre), Unit of Work (regrouper plusieurs save en une transaction), ou plus largement n'importe quel port piloté de l'hexagone (Vol 1 #2) — passerelle de paiement, envoi d'e-mail. Un seul principe : la dépendance va de la technique vers le métier.'The domain names what it wants, infra knows how to get it.' The same boundary, depending on what it hides, is called Data Mapper (translating between entity and table without either knowing the other), Unit of Work (grouping several saves into one transaction), or more broadly any driven port of the hexagon (Vol 1 #2) — payment gateway, email sending. One principle: dependency runs from the technical toward the domain.

Modèle mentalMental model
L'imageThe image

Un guichet de bibliothèque. Tu demandes « la commande 42 », on te la tend. Tu ignores si elle dormait sur une étagère, dans une réserve, ou dans un autre bâtiment : le rangement est le problème du bibliothécaire, pas le vôtre.A library desk. You ask for 'order 42', they hand it over. You've no idea whether it slept on a shelf, in a back room, or in another building: the shelving is the librarian's problem, not yours.

La règleThe rule

Le métier nomme ce qu'il veut en verbes du domaine — find, add, save, orders_to_ship() — et reçoit des entités ; une seule classe, derrière, sait que c'est une base et traduit dans les deux sens.The domain names what it wants in domain verbs — find, add, save, orders_to_ship() — and receives entities; one class, behind, knows it's a database and translates both ways.

Où l'analogie casseWhere the analogy breaks

Une vraie bibliothèque a un catalogue : on l'interroge de mille façons — par auteur, par date, en plein-texte. Un Repository, non : dès qu'on veut agréger, joindre, paginer pour un tableau de bord, reconstituer des entités une à une devient absurde, et la collection feinte se fissure. C'est exactement la faille qu'ouvre le #2 (CQRS) : lire et écrire ne veulent pas le même modèle.A real library has a catalog: you query it a thousand ways — by author, by date, full-text. A Repository doesn't: the moment you want to aggregate, join, paginate for a dashboard, rebuilding entities one by one becomes absurd, and the feigned collection cracks. That's exactly the fault #2 (CQRS) opens: reading and writing don't want the same model.

№ 04Le piègeThe trap4 min

Un objet nommé Repository ne suffit pas. Le faux qui laisse fuir la base.An object named Repository isn't enough. The fake that lets the database leak.

Le piège n'est pas d'oublier le Repository — c'est d'en écrire un qui ne cache rien. On crée une classe OrderRepository, on se sent en règle, mais ses méthodes rendent un query builder de l'ORM, ou prennent une chaîne SQL, ou exposent des noms de colonnes. Le métier, au-dessus, écrit toujours des filtres sur des tables : le couplage est intact, juste déguisé en abstraction. La frontière n'existe que si rien du stockage ne la traverse. Bascule le toggle : cherche le mot de persistance au-dessus de la ligne.The trap isn't forgetting the Repository — it's writing one that hides nothing. You create an OrderRepository class, you feel compliant, but its methods return an ORM query builder, or take a SQL string, or expose column names. The domain, above, still writes filters on tables: the coupling is intact, just disguised as an abstraction. The boundary only exists if nothing from storage crosses it. Flip the toggle: hunt the persistence word above the line.

# le Repository parle la langue du métier — il rend des entités, pas des lignes
class SqlOrderRepository(OrderRepository):
    def find(self, order_id) -> Order:          # ← rend une ENTITÉ métier
        row = self._db.query("SELECT ... WHERE id=%s", order_id)
        return Order(id=row["id"], status=row["status"], lines=...)   # SQL → domaine
    def save(self, order: Order):
        self._db.query("UPDATE orders SET status=%s ...", order.status)

# au-dessus de la frontière : aucun mot de persistance
class ShipOrder(repo: OrderRepository):
    def run(order_id):
        order = repo.find(order_id)   # pas de table, pas de colonne, pas de SELECT
        order.ship()                  # la règle vit dans l'entité, testable seule
        repo.save(order)
Pourquoi c'est justeWhy it's right

Le Repository rend une entité Order — pas une ligne, pas un curseur, pas un query builder. Au-dessus de la frontière, le métier dit find, ship, save : aucun nom de table, aucune colonne, aucun dialecte. Tout le SQL est captif d'une seule classe, en dessous, qui traduit dans les deux sens. Du coup la règle « on n'expédie que payé » se teste avec une collection en mémoire, et passer de Postgres à autre chose ne touche pas une ligne de métier. La frontière tient parce que rien du stockage ne la traverse.The Repository returns an Order entity — not a row, not a cursor, not a query builder. Above the boundary, the domain says find, ship, save: no table name, no column, no dialect. All the SQL is captive in one class, below, that translates both ways. So the rule 'only ship if paid' tests with an in-memory collection, and moving from Postgres to something else touches not one line of domain. The boundary holds because nothing from storage crosses it.

La règle qui tranche.The rule that settles it.

Aucun mot de persistance ne doit apparaître au-dessus du Repository. Le test : lis le code métier en cachant l'implémentation — y vois-tu un nom de table, une colonne, un SELECT, un type de l'ORM ? Si oui, la frontière fuit, peu importe le nom de la classe. Un vrai Repository expose des verbes du domaine (find, add, save, des requêtes nommées comme orders_to_ship()) et rend des entités. Ce qui entre et sort, c'est la langue du métier ; jamais celle de la base.No persistence word may appear above the Repository. The test: read the domain code with the implementation hidden — do you see a table name, a column, a SELECT, an ORM type? If yes, the boundary leaks, whatever the class is called. A real Repository exposes domain verbs (find, add, save, named queries like orders_to_ship()) and returns entities. What goes in and out is the domain's language; never the database's.

Le piège jumeau : le Repository générique.The twin trap: the generic Repository.

L'autre faux : Repository<T> avec un seul query(filter) qui prend n'importe quel critère. Il a l'air réutilisable, mais il n'abstrait rien — il déplace le query builder d'un cran sans le cacher, et le métier compose toujours des filtres techniques. À l'opposé, Active Record (l'entité qui se sauve elle-même : order.save()) assume le couplage au lieu de le déguiser — un choix honnête pour un CRUD simple, qu'on nommera en bilan. Le vrai Repository, lui, expose des requêtes du domaine, pas un moteur de requêtes générique.The other fake: Repository<T> with a single query(filter) taking any criterion. It looks reusable, but abstracts nothing — it moves the query builder one notch without hiding it, and the domain still composes technical filters. At the opposite end, Active Record (the entity that saves itself: order.save()) owns the coupling instead of disguising it — an honest choice for simple CRUD, which we'll name in the wrap-up. The real Repository exposes domain queries, not a generic query engine.

№ 05Bilan & éditoWrap-up & editorial2 min

Le métier qui écrit du SQL, ou le métier qui ignore sa base.The domain that writes SQL, or the domain that ignores its database.

Accès direct vs Repository, côte à côteDirect access vs Repository, side by side
SQL dans le métierSQL in the domainRepositoryRepository
Ce que le métier connaîtWhat the domain knowstable, colonnes, drivertable, columns, driverune collection d'entitésa collection of entities
Tester une règleTest a ruleune vraie base + fixturesa real DB + fixturescollection en mémoirein-memory collection
Changer de stockageChange the storerouvrir le métierreopen the domainune autre impl, métier intactanother impl, domain intact
La requête « charger X »The query 'load X'dupliquée, dériveduplicated, driftsune maison uniquea single home
Le prixThe pricegratuit, puis ingérablefree, then unmanageableune interface + un mapping à teniran interface + a mapping to maintain

Quand ne pas extraire un RepositoryWhen not to extract a Repository

Le Repository a un loyer : une interface, un mapping entité↔table à écrire et tenir, une couche de plus à traverser pour lire. On reste en accès direct — et c'est correct — quand :The Repository has rent: an interface, an entity↔table mapping to write and maintain, one more layer to cross when reading. Stay with direct access — and that's fine — when:

  • il n'y a pas de règle métier à protéger : un script d'export, un rapport, un job d'admin qui lit et recrache des lignes n'a aucune logique à isoler. Y glisser un Repository, c'est de la cérémonie (YAGNI, Vol 0 #3) — le SQL direct est plus honnête et plus court.there's no business rule to protect: an export script, a report, an admin job that reads and spits out rows has no logic to isolate. Sliding a Repository in there is ceremony (YAGNI, Vol 0 #3) — direct SQL is more honest and shorter.
  • l'entité EST le stockage, et tu l'assumes : pour un CRUD simple sans invariants, Active Record (order.save()) couple sciemment objet et table — moins de code, un couplage accepté les yeux ouverts. C'est un choix légitime tant que la règle métier ne dépasse pas la validation de champs.the entity IS the storage, and you own it: for simple CRUD with no invariants, Active Record (order.save()) knowingly couples object and table — less code, a coupling accepted with eyes open. A legitimate choice as long as the business rule doesn't exceed field validation.
  • le besoin de lecture est trop riche pour des entités : un tableau de bord qui agrège, joint et pagine n'a que faire d'objets Order reconstitués un par un. Là, une requête de lecture dédiée bat le Repository — et c'est exactement la fissure que le prochain numéro va creuser.the read need is too rich for entities: a dashboard that aggregates, joins and paginates has no use for Order objects rebuilt one by one. There, a dedicated read query beats the Repository — and that's exactly the crack the next issue will widen.

Le réflexe n'est donc pas « mets toujours un Repository » : c'est « y a-t-il une règle métier qui mérite d'ignorer sa base ? Si oui, glisse la frontière. Sinon, l'accès direct est plus court et plus franc. »So the reflex isn't 'always add a Repository': it's 'is there a business rule that deserves to ignore its database? If yes, slide the boundary in. If not, direct access is shorter and franker.'

Mot de l'éditeurFrom the editor

On ouvre le volume des données par la pièce la plus rassurante, parce qu'elle n'est pas neuve : le Repository, ce n'est que l'inversion de dépendance du Vol 1 pointée sur la persistance. Le métier possède le contrat, l'infrastructure l'implémente, la flèche va de la base vers le domaine. Si ce numéro ne laisse qu'une chose, que ce soit celle-là : « Repository » n'est pas un type d'objet à instancier, c'est une frontière que rien du stockage ne doit traverser. Le jour où un nom de colonne remonte dans une règle, la frontière a fui — quel que soit le nom de la classe.We open the data volume with its most reassuring piece, because it isn't new: the Repository is just Vol 1's dependency inversion aimed at persistence. The domain owns the contract, infrastructure implements it, the arrow runs from the database toward the domain. If this issue leaves only one thing, let it be this: 'Repository' isn't a kind of object to instantiate, it's a boundary nothing from storage may cross. The day a column name surfaces in a rule, the boundary leaked — whatever the class is called.

Mais le Repository fait une hypothèse tranquille qu'on n'a pas encore questionnée : qu'on lit et qu'on écrit le même modèle — la même entité Order pour décider d'expédier et pour afficher un tableau de bord. Le réflexe du volume était précisément « est-ce que je lis et j'écris le même modèle, et est-ce que ça me coûte ? ». Le #1 a répondu « range les deux derrière une frontière ». Le #2 va demander : et si lire et écrire avaient des besoins si différents qu'il vaille mieux deux modèles ? C'est la porte de CQRS.But the Repository makes a quiet assumption we haven't questioned yet: that you read and write the same model — the same Order entity to decide on shipping and to render a dashboard. The volume's reflex was precisely 'do I read and write the same model, and does it cost me?'. #1 answered 'put both behind a boundary'. #2 will ask: what if reading and writing had needs so different that two models were better? That's the door to CQRS.

Les voisins que ce numéro débloqueThe neighbors this issue unlocks

  • Unit of Work — le compagnon du Repository : il regroupe plusieurs save dans une seule transaction et suit ce qui a changé, pour qu'on commit ou rollback en bloc. Le Repository dit « où » ; l'Unit of Work dit « quand tout part ensemble ».the Repository's companion: it groups several saves into one transaction and tracks what changed, so you commit or rollback as a block. The Repository says 'where'; the Unit of Work says 'when it all goes together'.
  • Data Mapper — la mécanique sous le Repository : traduire entre l'entité et la table sans que l'une connaisse l'autre. C'est ce qui permet à Order de n'avoir aucune annotation de persistance — le mapping vit ailleurs, dans une couche qui ne fait que ça.the machinery under the Repository: translating between entity and table without either knowing the other. It's what lets Order carry no persistence annotation — the mapping lives elsewhere, in a layer that does only that.
  • Active Record — le contraire assumé : l'entité connaît sa table et se sauve elle-même (order.save()). Couplage volontaire, moins de cérémonie — excellent pour un CRUD sans invariants, dangereux dès que la règle métier grandit. Le nommer, c'est se rappeler que le Repository est un choix, pas un dogme.the owned opposite: the entity knows its table and saves itself (order.save()). Deliberate coupling, less ceremony — excellent for invariant-free CRUD, dangerous as soon as the business rule grows. Naming it is a reminder that the Repository is a choice, not a dogma.

Le kata — écris-le, ne le relis pasThe kata — write it, don't reread it

Tu as vu la frontière s'installer au chapitre 3 : le métier parle à une collection, une seule classe sait que c'est une base. Le geste qui l'ancre, c'est de l'écrire — et de prouver que la règle se teste sans base. Copie ce fichier dans ton langage habituel et fais-le passer au vert avec une implémentation en mémoire, sans jamais brancher de SQL. Le dernier test est le sournois : il exige que ta collection feinte copie ses entités comme le ferait un SELECT — sinon elle laisse passer un repo.save() oublié, qui, lui, casserait en production.You watched the boundary install itself in chapter 3: the domain talks to a collection, one class alone knows it's a database. The gesture that anchors it is writing it — and proving the rule tests without a database. Copy this file into your usual language and turn it green with an in-memory implementation, never wiring up any SQL. The last test is the sneaky one: it demands your feigned collection copy its entities the way a SELECT would — otherwise it lets a forgotten repo.save() slip through, which would break in production.

test_order_repository.py
# test_order_repository.py — la spec EST le harnais : fais-le passer au vert.
# le métier ne connaît qu'une collection ; une impl en mémoire suffit à tout tester.
# sans base, sans ORM, sans fixture SQL.

def test_add_puis_find():
    repo = InMemoryOrderRepository()
    repo.add(Order(id=1, status="paid"))
    assert repo.find(1).status == "paid"       # une collection : on range, on retrouve

def test_find_absent_rend_none():
    repo = InMemoryOrderRepository()
    assert repo.find(999) is None              # rien trouvé → None, pas d'exception

def test_la_regle_vit_dans_l_entite():
    o = Order(id=1, status="paid")
    o.ship()                                   # « on n'expédie que payé » : dans Order
    assert o.status == "shipped"

def test_regle_refuse_si_pas_paye():
    o = Order(id=1, status="pending")
    with raises(CannotShip): o.ship()          # invariant tenu sans toucher la base

def test_le_metier_ignore_le_stockage():
    repo = InMemoryOrderRepository()
    repo.add(Order(id=1, status="paid"))
    ShipOrder(repo).run(1)                      # le use case ne parle qu'au contrat…
    assert repo.find(1).status == "shipped"    # … et marche sur un dict en mémoire

def test_sans_save_rien_n_est_persiste():
    repo = InMemoryOrderRepository()
    repo.add(Order(id=1, status="paid"))
    o = repo.find(1)
    o.ship()                                   # on mute l'entité en mémoire…
    assert repo.find(1).status == "paid"       # …mais SANS repo.save(o), un find neuf ne
    # voit RIEN : la collection feinte doit COPIER comme un SELECT, sinon elle masque un
    # save oublié — un bug qui dort en test et casse en production.
Chez toi, cette semaine.In your codebase, this week.

Ouvre ta couche métier (services, use cases, entités) et cherche le stockage qui a fui : grep -rn "SELECT\|\.filter(\|session\.query\|\.objects\." src/domain src/services — chaque résultat au-dessus d'un Repository est une frontière qui fuit. Puis le test du chapitre 4 : lis une règle métier en cachant l'implémentation — y vois-tu un nom de table ou de colonne ? Si oui, ce n'est pas un Repository, quel que soit le nom de la classe.Open your domain layer (services, use cases, entities) and hunt the storage that leaked: grep -rn "SELECT\|\.filter(\|session\.query\|\.objects\." src/domain src/services — every hit above a Repository is a leaking boundary. Then chapter 4's test: read a business rule with the implementation hidden — do you see a table or column name? If so, it's not a Repository, whatever the class is called.