What can a hook that runs just before commit do that an after-commit callback cannot, and what does it cost?
answer
- which side of the boundary
- last chance to say no
- sees the whole change set
- writes there still need a flush
- locks are held while it runs
basics
~20 sA 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.
solid answer
~50 sThe two hooks differ in one respect that decides everything else: the before-commit hook is inside the boundary and the after-commit callback is outside it. Inside, the hook sees the complete set of changes the unit is about to commit — which is what makes it the right place for a validation that spans several objects, for stamping audit fields, and for adding a row that must land with the rest. Throwing there aborts the commit, so the hook has genuine veto power. The costs are real: everything it does extends the transaction and keeps its locks held, layers differ in whether writes made inside the hook are picked up by an extra flush or need one requested, and an external call placed there is still on the wrong side of the commit and can announce a change that never lands.
go deeper
Learn the position first: the before-commit hook is still inside the transaction, so throwing there means nothing is written. The after-commit callback is outside, and cannot stop anything.
Explain what the hook can see and change — the full pending change set — and why that makes it the right place for cross-object validation and derived writes. Mention that its writes still have to be flushed.
Show that you count the cost in lock hold time and that you refuse external calls there. Be able to describe how a missing derived row traces back to flush behaviour rather than to the hook not running.
Treat the hook as a scarce resource with a policy attached: database work only, bounded time, no inter-hook dependencies. Push anything reaching outside into data written inside the unit and delivered afterwards.
## Two hooks, one boundary Most data-access layers expose two points around the end of a unit of work: one that runs **just before** the commit is issued, and one that runs **after** the commit returns. Everything that distinguishes them follows from which side of the boundary they sit on. | | Hook before commit | Callback after commit | |---|---|---| | Transaction state | Open, about to commit | Ended, already durable | | Sees the unit's pending changes | Yes, in full | No — the tracked set is gone | | Can add writes to the same commit | Yes | No | | Can veto | Yes, by throwing | No | | Effect of its failure | The whole unit is discarded | The data stays; the effect is lost | | Adds to lock hold time | Yes | No | ## What the before-commit hook is genuinely good for 1. **A validation that needs the whole picture.** Per-object checks belong on the object. But a rule like "the sum of the lines must match the header" or "this unit may not both close an account and add a charge to it" only has its inputs once every change in the unit is known. The hook runs at exactly that moment. 2. **Deriving state from the change set.** Stamping who changed what and when, computing a totals row, marking a parent as touched because a child moved — all of these are writes that must be in the same commit, and all of them need the change set to exist first. 3. **Writing the record of an effect.** If a change must be announced elsewhere, the record of that intent can be written here as an ordinary row, so that it commits with the change or vanishes with it. 4. **Refusing the transaction.** Throwing from the hook means the commit is not attempted and the unit is discarded — a veto with the same force as a failure anywhere else inside the boundary. ## The costs, stated plainly - **Lock hold time.** Every millisecond the hook spends is a millisecond the rows the unit already wrote stay locked. A hook that does a network call, a slow computation or a broad query converts a short transaction into a long one, and long transactions are what turn a healthy system into a queue of blocked writers. - **Write visibility inside the hook.** A write made from the hook must still be flushed before the commit. Layers differ: some flush again after running the hooks, some require the flush to be requested, and some restrict what the hook may modify at that point. Assuming the friendliest behaviour is how a "the audit row is missing sometimes" bug is born. - **Ordering between hooks.** Once several hooks are registered, one hook's writes can be another hook's input. Ordering is layer-specific and rarely obvious, so hooks that depend on each other's output are fragile by construction. - **External calls are still premature.** This is the trap that looks safest and is not. Being inside the transaction does not make an outbound call retractable. The commit can still fail after the message has left, and now the outside world knows about a change that was rolled back. Inside the boundary, only **database work** enjoys the boundary's protection. - **Surprising failures.** A veto raised at the very end aborts work the caller believed was already accepted. That is correct behaviour, but it means the error must be a clear, expected one rather than an unhandled failure surfacing from deep inside the layer's shutdown path. ## Choosing between the two Ask what the work needs from the transaction: - It needs to be able to **stop** the change, or to be **written with** it → before commit. - It needs the change to be **certain** before it happens → after commit. - It needs both — atomic with the change *and* reaching the outside world → **neither hook can give you that.** The resolution is to split the effect: write the intent as data inside the unit, and let a separate step perform it once the data is durable. ## The compression A before-commit hook is the last moment you can still say no, and the last moment you can still add to the commit. An after-commit callback is the first moment you can safely tell anyone. Work placed on the wrong side of that line either loses its power to veto or gains the power to lie.
- Why is a cross-object validation better placed in a before-commit hook than at each save call?Because at save time the unit is incomplete: the object that would make the rule pass or fail may not have been touched yet, so the check either runs too early or has to be repeated defensively. The hook runs once, at the only moment when the full set of pending changes is known, and its failure still discards everything.
- What goes wrong when a before-commit hook makes a network call?Two things. The call happens while the transaction still holds its locks, so a slow or hanging endpoint stretches the transaction and blocks other writers. And the commit can still fail afterwards, so the call may have announced a change that never lands — the exact ordering problem the after-commit position exists to avoid.
saying these in an interview costs you the question
- Thinks a before-commit hook runs after the data is durable
- Calls an external system from the hook and considers it safe
- Assumes writes made in the hook are always flushed automatically
- Relies on a specific ordering between independently registered hooks
- Believes throwing in the hook discards only the offending object