In a data-access layer, what does an after-commit callback guarantee that code placed right after the last save does not?
answer
- durability has one exact moment
- flush is not commit
- in line still means inside
- callback runs on the success path only
- one-way door, no influence back
basics
~20 sAn after-commit callback runs only once the transaction has actually committed, so its side effect can never announce a change that later disappears. Code written in line after the last save still sits inside the open, undecided transaction.
solid answer
~40 sA save call inside a transaction only stages a change in the layer's tracked set, and even after the layer flushes it into SQL the rows are still uncommitted: a later statement, a check at commit time, or a failure further up the call stack can throw all of it away. So any external effect written in line after the last save — an email, a cache eviction, a notification — can fire for a change that never becomes durable. Registering that effect as an after-commit callback hands it to the layer, which runs it only after the commit returns successfully. The trade is direction of influence: the callback can no longer affect the transaction, and if the callback itself fails, the committed data stays committed and only the effect is lost.
go deeper
Remember the order: save stages, flush emits SQL, commit makes it real. Anything that must not happen for a change that never lands has to wait for the commit to return.
Explain why flush is not durability, and name what can still fail between the last save and the commit. Then explain what registering a callback actually changes about when the layer invokes your code.
Show that you weigh the two failure directions before choosing a placement: announcing something false versus losing something true. Say out loud that a callback failure leaves committed data untouched and needs its own recovery path.
Frame it as which inconsistency the business can absorb and for how long. Decide when the effect is cheap enough to lose, and when it must be written as data inside the transaction and delivered separately.
## What a save call actually does In a data-access layer that tracks loaded and new objects, the call that "saves" an object does not write a row. It registers that object in the **tracked set** — the collection of objects the current **unit of work** is responsible for. At some later point the layer **flushes**: it turns the pending state into SQL statements and sends them over the connection the transaction is running on. Flush is not durability. Those statements have executed inside a transaction that has not ended. Other transactions generally cannot see the effect yet, the engine is still holding the locks and undo information needed to take it all back, and a rollback erases the work without a trace. Durability arrives at exactly one instant: when the **commit** returns successfully. Everything before that instant is provisional. ## Why "in line after the last save" is the wrong place Between the last save and the commit, plenty can still go wrong: - a later statement in the same unit violates a constraint; - a constraint the engine deferred to the end of the transaction fires at commit; - a version check finds zero rows updated and the unit is abandoned; - code further up the call stack throws, and the boundary rolls back; - the commit itself fails — the connection drops, a lock wait times out, the engine cannot write its log. A side effect placed before that point has announced a fact the database may never adopt. Worse, the receiver has no channel to learn it was retracted: an email cannot be unsent, a cache eviction has already happened, another system has already recorded the news. ## The three placements, side by side | Placement | Runs when | Can it still change the transaction? | Characteristic failure | |---|---|---|---| | In line after the last save | Inside the open transaction | Yes — an exception aborts the unit | Announces a change that may roll back | | Hook just before commit | Inside, at the very end | Yes — it can still veto | Same, plus it lengthens the transaction | | After-commit callback | After the commit returns | No | The effect can be lost with no trace | ## What the callback does and does not guarantee It **guarantees ordering against durability**. The layer keeps a list of registrations made during the unit and walks it after the commit succeeds; if the transaction rolls back, the list is simply discarded and nothing runs. That is the whole value: the effect exists only in the world where the change exists. It does **not** make the effect reliable. Three consequences follow from running outside the boundary: 1. **A failing callback cannot undo the commit.** The data stays; only the effect is lost. Recovering it requires a new write, not a rollback. 2. **Nothing retries it for you.** Most layers invoke the registration once and, if it throws, log it or let the exception escape past the boundary. Layers differ in whether that exception reaches the caller at all, which is a reason not to hide important work there. 3. **The unit of work is finished.** The tracked set is closed, so the callback must not try to resolve a link that was never loaded, and must not write through the same unit. Whatever the effect needs — identifiers, values, a snapshot of the change — has to be captured while the unit was still open. ## Choosing a placement deliberately The question to answer before writing the effect is: **which failure can this system absorb?** 1. If a false announcement is worse than a missing one — money moved, an irreversible notification — place it after the commit and accept that it can be lost, or make the notification itself part of the transaction by writing it as a row in the same unit of work. 2. If the effect must be able to stop the change — a last validation across everything the unit touched — it belongs before the commit, where throwing still means nothing is written. 3. If the effect is cheap to redo and reconstructible from committed state, such as refreshing a cached value, an after-commit callback with a lost-on-crash risk is usually fine. ## The shape to remember A commit is a one-way door. Anything written before it is a promise about a transaction that has not finished; anything written after it is a fact the system cannot retract. An after-commit callback is the layer's way of letting you say "do this on the far side of the door" — nothing more, and nothing less.
- If the after-commit callback throws, what happens to the data that was just committed?It stays committed. The transaction ended before the callback started, so nothing about the failure can revoke it; correcting the data requires a new write in a new unit of work. Layers differ in whether the exception reaches the original caller or is only logged, so an important effect placed there needs its own recording or retry, not just a stack trace.
- Does a successful flush mean other transactions can see the change?No. Flush only pushes the pending statements onto the connection inside the same transaction. Under the usual isolation levels other transactions still read the pre-change state until the commit returns, and a rollback removes the work entirely. Flush controls when the SQL runs and when generated keys or constraint errors appear to your code — not when the change becomes public.
- Where should the effect go if it must survive a crash right after the commit?Out of the callback and into the transaction, as data. Writing the intent as a row in the same unit of work makes the state change and the record of the effect commit or roll back together; a separate process then performs it. That converts an effect that can vanish into one that is merely delayed.
saying these in an interview costs you the question
- Thinks the save call itself makes the row permanent
- Believes a successful flush means the data is committed
- Sends the notification straight after the save and calls it done
- Assumes a failing after-commit callback rolls the commit back
- Thinks code at the end of the method body already runs after commit
- Treats an after-commit callback as a delivery guarantee