skip to content

Transactions from the Application

Where a transaction boundary sits, how it propagates, whether a conflict is caught by a version check or a lock, and what may run after commit. Interviewers probe it because bugs here corrupt data.

on this pageshow

explore

questions

page 1 of 2

In a data-access layer, what does an after-commit callback guarantee that code placed right after the last save does not?

level: juniorimportance: must knowfreq 70%

answer

  1. durability has one exact moment
  2. flush is not commit
  3. in line still means inside
  4. callback runs on the success path only
  5. one-way door, no influence back

basics

~20 s

An 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 s

A 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

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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
open as a page

Why is a transaction boundary usually placed around one use case rather than around each repository call?

level: juniorimportance: must knowfreq 72%

basics

~20 s

A transaction commits or rolls back as a whole, so the boundary must enclose everything that has to be all-or-nothing - usually one use case. Per-call commits make each write permanent on its own, so a later failure leaves half-written data.

open as a page

Why does a data-access layer translate database errors into its own exception types instead of exposing vendor codes?

level: juniorimportance: must knowfreq 64%

basics

~20 s

A translation step maps engine-specific codes and messages onto a small portable set of failure kinds - constraint violation, deadlock victim, serialization failure, lock or statement timeout, lost connection - so calling code branches on the kind, not on a number.

open as a page

When a data-access layer loads an object with a pessimistic lock requested, what changes about the read, and how long does the lock last?

level: juniorimportance: must knowfreq 62%

basics

~20 s

The layer turns the read into a locking read, so the row is claimed for your transaction as it is fetched. Other writers of that row wait. The lock is released only when the transaction boundary ends.

open as a page

What does marking a unit of work read-only change about how a data-access layer handles the objects it loads?

level: juniorimportance: must knowfreq 60%

basics

~20 s

A read-only unit stops keeping a value snapshot of each loaded object, so no dirty check and no flush run at the end. Reads get a lighter, cheaper path, and anything modified inside the unit is simply not written.

open as a page

When an operation with its own declared boundary is called inside an existing transaction, what propagation choices exist?

level: middleimportance: must knowfreq 70%

basics

~20 s

Propagation decides what an inner bounded call does when a transaction is already running: join it, suspend it for an independent one, refuse to run unbounded, or open a nested scope that can be discarded on its own.

open as a page

When a transaction boundary is applied by a wrapper around an object, why can a call between its own methods run untransacted?

level: middleimportance: must knowfreq 66%

basics

~20 s

Interception happens as a call passes through the wrapper. A method calling another on the same object uses the object's own reference, so the wrapper is never crossed, no boundary begins, and the inner method's declared attributes are silently ignored.

open as a page

In a mapper with a tracked set, why is catching a failed flush and continuing with the same unit unsafe?

level: middleimportance: must knowfreq 57%

basics

~20 s

A rejected flush leaves the tracked objects out of step with the stored rows and normally condemns the unit, so anything written afterwards is discarded at commit. The unit has to be abandoned and rebuilt, not repaired in place.

open as a page

In a data-access layer that maps a version attribute, what does it do on each update, and how does a conflict surface?

level: middleimportance: must knowfreq 78%

basics

~20 s

The layer puts the version value it loaded into the update's where clause and advances the version in the same statement. If the statement reports zero rows changed, another writer moved the row on, and the layer raises a conflict error.

open as a page

A unit of work already holds a loaded copy of a row; why must a later pessimistic lock request force a re-read?

level: middleimportance: must knowfreq 55%

basics

~20 s

The copy in the tracked set was read without a lock, so another transaction may have committed since. A lock taken without re-reading protects a row whose current values you have never seen. Your write then loses that change.

open as a page

When a data-access layer must notify another system about a change, how do publishing before the commit and publishing from an after-commit callback fail differently?

level: seniorimportance: must knowfreq 66%

basics

~20 s

Publishing before the commit risks a false positive: the notification survives a rollback and announces a change that never landed. Publishing afterwards risks a false negative: the change is durable but the notification can vanish.

open as a page

In a data-access layer, where must a retry of a failed unit sit, and what must each attempt rebuild?

level: seniorimportance: must knowfreq 58%

basics

~20 s

Outside the transaction boundary. Each attempt starts a new unit with an empty tracked set and reloads the data it works on, because objects from the failed attempt belong to a rolled-back transaction. Bound the attempts and keep the operation idempotent.

open as a page

How does a version value survive the round trip when a record is read, edited in a client, and posted back?

level: seniorimportance: must knowfreq 60%

basics

~20 s

The version travels out in the transfer model and comes back with the submitted edit, so the write compares what the user actually saw. A path that re-reads the row and copies fields onto it compares nothing.

open as a page

When each tenant has its own datasource or schema, what must the data-access layer do before a unit of work starts and after it ends?

level: seniorimportance: must knowfreq 55%

basics

~20 s

Resolve the tenant and bind its datasource or schema before the first statement, since the connection is fixed for the unit's life. On release, reset any state the switch set, or the pool lends the connection out still pointed there.

open as a page

A request writes a row, then a read-only unit routed to a replica reads it back stale - what causes this and what fixes it?

level: seniorimportance: must knowfreq 58%

basics

~20 s

Replication is asynchronous, and each unit is routed on its own declared attributes with no memory that an earlier unit in the request wrote. The fix is a request-scoped pin sending later units to the primary.

open as a page

What can a hook that runs just before commit do that an after-commit callback cannot, and what does it cost?

level: middleimportance: should knowfreq 50%

basics

~20 s

A before-commit hook still runs inside the transaction, so it can inspect the unit's whole change set, add writes to it, and veto the commit by throwing. The cost is a longer transaction holding locks, and external calls there stay unsafe.

open as a page

Why must an after-commit callback avoid resolving a deferred link or writing through the unit of work that just committed?

level: middleimportance: should knowfreq 58%

basics

~20 s

By the time an after-commit callback runs, its unit of work is finished. The tracked set is closed, so a link that was never loaded has nothing left to resolve it, and a write has no transaction to join.

open as a page

In a data-access layer, how does declarative transaction demarcation differ from opening the boundary programmatically?

level: middleimportance: should knowfreq 60%

basics

~20 s

Declarative demarcation attaches the boundary to a method as metadata and lets a wrapper begin, commit or roll back around the call. Programmatic demarcation writes those steps in the code, buying exact scope and runtime-chosen attributes at the cost of boilerplate.

open as a page

Why can a caller that swallowed a data-access failure still get a rollback error when the boundary commits?

level: middleimportance: should knowfreq 49%

basics

~20 s

Because the failure already marked the unit rollback-only. That flag lives on the transaction, not on the exception, so catching and discarding the exception leaves the unit condemned and the boundary refuses to commit it.

open as a page

In a unit of work that defers writes, when does a version conflict surface, and why does the timing matter?

level: middleimportance: should knowfreq 52%

basics

~20 s

A version conflict appears only when the guarded statement runs - at a flush inside the unit, or at commit - never when the object is changed in memory, so the error can arrive after the service method has returned.

open as a page

As a row's version value, how does an integer counter compare with a timestamp, and when does the timestamp break?

level: middleimportance: should knowfreq 42%

basics

~20 s

A counter only has to differ from the previous value, so it is exact and cheap. A timestamp also serves as a last-changed column but breaks on two writes in one clock tick, on clock skew, or on precision lost in transit.

open as a page

When a data-access layer locks a loaded object, what does that lock cover for its deferred references and child collections?

level: middleimportance: should knowfreq 44%

basics

~10 s

Only the rows the layer read to build that object. An unloaded reference and child-collection rows are separate rows, so they can change while you hold the lock unless you request an extended scope.

open as a page

How does a routing data-access layer decide to send a unit of work to a replica, and when is that decision fixed?

level: middleimportance: should knowfreq 48%

basics

~20 s

The read-only marking declared on the unit is the usual routing signal, and it is read before the unit takes a connection. That connection then points at one server for the unit's whole life, so nothing can re-route it later.

open as a page

What happens to an object that code modifies inside a unit of work that was marked read-only?

level: middleimportance: should knowfreq 52%

basics

~10 s

Usually it is discarded: with no snapshot and no dirty check, the layer emits no update and the unit commits having written nothing. Some layers instead declare the transaction read-only and reject the write.

open as a page

Why declare a timeout on a transaction boundary, and what happens to the work when that timeout expires?

level: seniorimportance: should knowfreq 48%

basics

~20 s

A timeout caps how long one unit may hold locks and a connection, so one stuck statement cannot pile callers up behind it. When the deadline passes the unit is failed and rolled back in full - nothing partial survives.

open as a page

A flush reports a violated unique constraint. How should the write path turn that into a specific business error?

level: seniorimportance: should knowfreq 46%

basics

~20 s

Read the constraint name from the translated failure and map it through a table the application owns, never by parsing the message. Because the unit is condemned, build the response from values captured before the flush, after the rollback.

open as a page

Which writes escape a version guard, and how do you keep the guard meaningful when such writes are needed?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Any statement not emitted for a tracked object - a set-based update, a hand-written fix, another service - neither compares nor advances the version unless it says so. Write the increment into those statements and re-read anything already loaded.

open as a page

Why can the order your code touches objects differ from the order a deferring data-access layer actually acquires their row locks?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Because changes are buffered and sent at flush, and the layer orders those statements by its own rules rather than by the order your code made them. Locks are taken when statements run, so the flush order is the acquisition order.

open as a page

A pessimistic lock request through a data-access layer can block indefinitely; what bounds that wait, and how must callers handle the outcome?

level: seniorimportance: should knowfreq 47%

basics

~20 s

By default the request queues for as long as the holder keeps the row. A wait timeout or an immediate-failure request bounds it, turning the block into an error that rolls the unit of work back.

open as a page

How do you decide whether a use case with several independent sub-operations runs under one boundary or one per sub-operation?

level: principalimportance: should knowfreq 44%

basics

~20 s

Start from the invariant: only a state no reader may ever observe justifies one boundary. Otherwise weigh a long unit's lock time against the obligation a split creates - an intermediate state that must be detected, resumed and safely repeated.

open as a page

showing 1–30 of 34