Olivier Lowe

This technical article is available in English.

The In-Memory Lie

What a Fake Can Prove — and What It Can't

The Test That Was Already GREEN

In NovaTrade's backend, the Application test for placing an Order already passed. It used an in-memory Repository. The use case retrieved an Order, asked it to place itself, and the test observed the PLACED state.

That GREEN was useful evidence. The Application could coordinate Order placement without knowing about Django, ORM models, database tables, or SQL. The business transition did not require the framework to participate in the experiment.

The mistake was extending that result into a second claim: that the changed Order would also be durable when the same workflow used a database.

Why the Fake Looked Like Persistence

The fake stored Python Order objects in a dictionary. Its get operation returned the same live object stored there. This conceptual excerpt shows the relevant behavior; the examples in this article are illustrative, not verbatim repository code.

class FakeOrderRepository:
    def __init__(self):
        self._orders = {}

    def get(self, order_id):
        return self._orders[order_id]

Suppose the dictionary holds ORDER-123 in DRAFT. Retrieving it does not create a copy. The local variable and the dictionary refer to the same object, so calling order.place() changes the object visible through both references.

FakeOrderRepository
ORDER-123 -> Order(DRAFT)
                 |
              place()
                 |
                 v
             Order(PLACED)

order = repository.get(order_id)
order.place()

ORDER-123 -> Order(PLACED)

No separate persistence operation occurred. Reading the fake again merely exposed the mutation of the object it already held. That appearance of persistence is the in-memory lie: a conclusion supplied by the reader, not a promise made by the fake.

Then Django Entered the Experiment

The Django adapter crossed a different boundary. It read persisted data and used it to reconstruct a new Domain object. The resulting Order was not a live Python reference to a database row.

Database: DRAFT
  ↓
Django ORM
  ↓
DjangoOrderRepository
  ↓
Order.reconstitute(...)
  ↓
Order(DRAFT)
  ↓ order.place()
Order(PLACED)

Database: still DRAFT

The Domain transition succeeded, but mutating that reconstructed Aggregate did not write anything back. The integration test made the separate persistence claim observable by reading stored state again after the use case ran.

persist DRAFT Order
  ↓
run place_order()
  ↓
read persisted state again
  ↓
expect PLACED

Expected: PLACED
Actual:   DRAFT

A fresh read exposed what an assertion on the already-mutated Domain object could not: the database still contained DRAFT.

The Fake Wasn't Wrong

Fakes are not inherently bad. This fake allowed the Application test to exercise coordination independently of Django. It gave a fast, focused answer to a useful question.

It had not exercised reconstruction from database data or a durable write through the Django adapter. Its GREEN result could not establish either behavior. The passing Application test and failing integration test were not contradictory: they supplied evidence for different claims.

Different Tests Prove Different Things

I separate the question about the business transition from the question about coordination, and both from the question about durable storage. Each experiment needs to observe the behavior its claim depends on.

Test boundaries and the questions they answer
TestQuestion
Domain test Does the business rule behave correctly?
Application test + fake Does the use case coordinate the capabilities it requires?
Django integration test Does the real persistence adapter provide the expected durable behavior?

The integration test earns its persistence claim by observing a fresh read after the workflow. The Application test earns its coordination claim without needing to know how Django implements that capability.

The Wrong Fix Would Have Been a Smarter Fake

Making the fake imitate Django would move attention away from the missing capability. Fake serialization, ORM simulation, identity maps, automatic dirty tracking, and database constraint simulation would introduce mechanisms this experiment did not require.

A database simulator would still not prove that the real Django adapter writes correctly. That evidence belongs to the integration test. The observed failure earned an explicit save operation, not a Unit of Work or a more elaborate fake.

What Actually Changed

The missing Repository capability became explicit: repository.save(order). The Application now expressed the complete workflow: retrieve the Aggregate, perform the business transition, and save the changed Aggregate.

def place_order(order_id, repository):
    order = repository.get(order_id)
    order.place()
    repository.save(order)

# Coordination sequence:
# repository.get() → order.place() → repository.save()

The Domain owns the business transition. The Application owns orchestration. The Django adapter owns how persistence happens. The Order did not acquire a database dependency or become responsible for saving itself.

Conceptually, the adapter maps the changed Domain state back to an ORM model and performs the write. This excerpt illustrates that responsibility, rather than reproducing the current adapter implementation.

def save(self, order):
    model = OrderModel.objects.get(id=order.id)
    model.status = order.status
    model.save()

The explicit write completes the path from persisted DRAFT data to persisted PLACED data. The fresh read in the integration test can now observe the changed durable state.

Database: DRAFT
  ↓ repository.get()
Order(DRAFT)
  ↓ place()
Order(PLACED)
  ↓ repository.save()
Database: PLACED

The fake also provides save. Its conceptual implementation remains small:

def save(self, order):
    self._orders[order.id] = order

Because the fake still holds live references, this assignment may add no new observable behavior inside it. That is okay. Its observable contract matters; it does not need Django's internal mechanisms. The Application now states the workflow it requires: get → change → save. The integration experiment supplies the separate evidence that the real adapter makes that change durable.

The Question I Ask Now

“Is this test realistic enough?” is too broad to guide this decision. I now ask: “What claim is this test actually capable of proving?”

If the claim concerns coordination, I inspect the capabilities the use case exercises. If it concerns persistence, I look for an experiment that crosses the real adapter boundary and observes durable state. A GREEN result becomes useful when I can name both the claim and the observation supporting it.

The Lesson

A fake is not dangerous because it simplifies reality. The danger begins when its GREEN result is treated as evidence for behavior that experiment never exercised.

The earlier Application test retained its value. The Django integration test exposed a separate missing capability. One explicit save operation answered that pressure while keeping the business transition, coordination, and persistence mechanism with their respective owners.

GREEN is evidence — but only for the claim the experiment actually tested.

CONTINUE THE ARCHITECTURE JOURNEY

The experiment is only part of the story.

This is one experiment from my book Building Software Around the Business — Not Around the Framework — Backend Architecture with Python, Django, and Flask.

In the book, I walk through the complete development path: the first Flask RED, the boundary decisions, error translation, the system-level proof, why Django remained in persistence, and what the experiment still could not prove.

Explore the backend architecture book →