THIS EXPLANATION
THE ROOM
CDA·42 Computing, Data & AI 6 MIN · 8 STATIONS

Collecting changes before committing them

A Socratic walk-through of collecting changes before committing them — reasoned out one step at a time, not lectured.

abcdefgh
a

The question we started with

THE QUESTION #

Why gather up every edit and write them all at the end rather than as they happen?

A piece of business logic touches six objects: it debits an account, credits another, marks two invoices paid, creates a receipt, and deletes a stale reservation. The straightforward implementation writes each change as it is made. Six statements, in the order the code happens to reach them.

The alternative looks strictly worse at first: hold every change in memory, remember what was created, altered and removed, and write nothing until the very end. That is more machinery, more state, and a window in which the database and the program disagree. It is worth asking what could possibly justify it — and the answer turns out not to be performance, though performance is the reason usually given.

b

Reasoning it through

REASONING #

Begin with the immediate-write version and ask what goes wrong. Suppose the fifth step raises. Four writes have already landed. If they were inside one database transaction, the rollback saves you — but now the transaction has been open for the whole duration of the business logic, holding locks while your code does whatever else it does. If they were not in one transaction, you have a half-finished operation in the store and no automatic way back.

So the first thing deferral buys is not speed but a narrower window. If nothing is written until the end, the transaction opens late, does its writes back to back, and closes. The time locks are held shrinks to the time writing actually takes, rather than the time the business logic takes.

Now ask a second question: does the order of writes matter? It does, and more than the code's own order knows. A row that references another cannot be inserted first; a parent cannot be deleted while children point at it. Code written in business order will violate those constraints at random. But an object that has collected all the changes can sort them — inserts before updates before deletes, parents before children — because it can see the whole set at once. Nothing scattered through the business logic could make that decision, because none of it knows what the others did.

Third, consider repetition. Business logic frequently touches the same object several times: a customer's balance adjusted, then adjusted again, then a flag set. Written eagerly, that is three round trips. Collected, it is one row's worth of final state, written once. And this is where the deduplication requires something extra — to know that two references to "customer 4711" are the same object rather than two copies, the collector needs an identity map: a registry ensuring one in-memory instance per identity within the unit. Without it the two copies would overwrite each other with different values, and the last write would win arbitrarily.

Which gives the pattern its name. Fowler catalogued it in Patterns of Enterprise Application Architecture (2002) as the Unit of Work: an object that keeps a list of everything affected by a business transaction, works out what must be written, and coordinates writing it — along with the concurrency check. That last part matters: if each object carries a version number, the unit can verify at flush time that nobody else changed the rows since they were read, and fail the whole thing if someone did. Checking versions one write at a time gives you no such guarantee, because the earlier checks are already stale by the time the later ones run.

So has this bought us anything a plain database transaction would not? Partly the same thing, at a different level. A transaction gives atomicity in the store; the unit of work gives you an atomic boundary in the program, before anything is sent. That is why an in-memory unit can be abandoned with no rollback at all — nothing was ever written — and why it can inspect, order, and collapse the changes in a way a stream of statements already in flight cannot.

The cost is a real one, and it is conceptual rather than computational. Your code now runs in a world where an assignment does not mean a write. Query the store mid-way through and you may not see your own changes — which is why ORMs such as Hibernate, Entity Framework and SQLAlchemy add autoflush: a hidden write before a query, so the query sees pending work. That is a sensible fix, and also the source of the pattern's most confusing bugs, since a write can now happen at a moment nothing in your code names.

c

The analogy

THE ANALOGY #
THE FIGURE

Think of an editor working over a manuscript in pencil, marking every change on the page, and only then handing the marked-up copy to the typesetter to be reset in one pass. Working directly on the plates instead would mean a compositor standing by all afternoon, changes made in whatever order the editor thought of them, and a half-reset page if the editor stopped for lunch.

WHERE IT BREAKS DOWN

a pencilled manuscript is a genuinely separate document, whereas a unit of work is tracking the live objects the program is still using — so unlike the editor, the program cannot look at the unmarked original, and any code that reads those objects mid-transaction already sees the pending state whether it wants to or not.

d

Clarifying the model

THE MODEL #

Three clarifications.

The unit of work is not the transaction. It is a plan for one, assembled in memory and then executed inside one. Conflating them is what leads people to expect a rollback to restore their in-memory objects: it does not. The database is returned to its previous state; the objects in the program are left holding the values that failed, which is a well-known source of surprise after a failed commit.

Deferral is not free of visibility problems, and "flush" is not "commit". A flush sends the collected statements to the database; a commit ends the transaction. Between the two, the changes are visible to your own session and to nobody else — and an explicit flush without a commit is a common way to get a generated identifier early while remaining able to abandon everything.

Finally, the pattern earns its keep in proportion to how many objects one operation touches. For a single-row update it is pure overhead, and reaching for it there is how a small application acquires machinery it will never use.

e

A picture of it

THE PICTURE #
Collecting changes before committing them
Collecting changes before committing them The self-loop on Tracking is where all the business logic happens -- every edit lands there and nowhere else, which is why the path out through Abandoned costs nothing. The three states after the commit request are the work the collection made possible: ordering the statements, sending them inside one transaction, and checking versions as a single verdict rather than one at a time. {"generator":"mermaid-svg-renderer@3.2.1","source":"../Socrates/.diagram-cache/_src/collecting-changes-before-committing-them.md","sourceIndex":1,"sourceLine":4,"sourceHash":"4e3fcc85d23f768535b4502fcb50d5ad5c33b78108b5111a78d910b4667247d1","diagramType":"stateDiagram","layoutVariant":"source","repairedDuplicateIds":[],"motion":"entrance-with-reduced-motion-fallback","presentation":"editorial","attempt":1,"viewBox":{"x":0,"y":0,"width":864,"height":1233},"qa":{"passed":true,"findings":[]}} business operation begins first object read, created,or modified more objects registeredas new, dirty, or removed commit requested changes sorted,duplicates collapsed statements sent insideone transaction no other session touchedthese rows a version number movedunderneath us operation abandonedbefore commit Clean Tracking Ordering Writing VersionCheck Committed Failed Abandoned Nothing was ever sent.No rollback is needed.
KINDSconnectornegative branch

How to readThe self-loop on Tracking is where all the business logic happens — every edit lands there and nowhere else, which is why the path out through Abandoned costs nothing. The three states after the commit request are the work the collection made possible: ordering the statements, sending them inside one transaction, and checking versions as a single verdict rather than one at a time.

f

What became clearer

WHAT CLEARED #
WHAT CLEARED

Collecting the changes is not primarily an optimisation. It creates a moment at which the whole set of intended changes exists in one place — and only from that vantage point can they be ordered correctly, collapsed, checked for concurrent interference together, or discarded for free. The price is that assignment and persistence come apart, and you must always know which one you are looking at.

g

Where to go next

ONWARD #
  • Optimistic versus pessimistic concurrency, and why the deferred write suits the former.
  • How autoflush decides when a query needs pending changes made visible.
  • Whether the identity map's guarantee survives across units of work, and what happens when it does not.
h

Key terms

TERMS #
TermWhat it means
Unit of Workan object that records everything a business operation changed and coordinates writing it all out in one transaction.
Identity mapa registry guaranteeing that a given stored entity is represented by exactly one in-memory object within a unit of work.
Flushsending the collected statements to the database, distinct from committing the transaction that contains them.
Optimistic concurrencydetecting a conflicting concurrent change at write time, typically via a version number, rather than preventing it with a lock.

Every term the collection defines is gathered in the glossary.

Nearby on the shelf

4