skip to content

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%

answer

  1. no statement, no check
  2. flush or commit, not assignment
  3. an automatic flush hides a query trigger
  4. commit-time errors escape the method body

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.

solid answer

~40 s

Changing a field on a tracked object does nothing at the database; the guarded update is emitted later, and the conflict is discovered at that moment. In a layer that defers writes, that moment is either a **flush** - forced explicitly, or triggered automatically before a query that must see pending changes - or the **commit** at the end of the unit. Timing decides where the failure lands: a conflict raised at commit escapes any handler wrapped around the service body, and it lands after side effects that ran earlier in the method have already happened. If a use case has to react to a conflict in place, flush at that point on purpose so the check runs where you can still handle it and nothing irreversible has been done yet.

go deeper

for a junior

Setting a field on a loaded object does not talk to the database. The comparison that detects a clash happens later, when the layer actually sends the update.

for a middle

Name the three moments that emit pending writes - an explicit flush, an automatic flush before a query, and commit - and say which of them your handler can see.

for a senior

Show you order side effects around detection: nothing irreversible before the write, a deliberate flush when the use case has to react in place, and logging that records which row lost.

for a principal

Treat late detection as a design constraint on the boundary: decide per use case whether the cost of an early flush is worth precise handling, and whether conflicts are a metric anyone watches.

## The check runs with the statement, not with the assignment When application code sets a field on an object the layer is tracking, no statement is sent. The object is simply marked as changed. The guarded update - the one whose `WHERE` clause carries the loaded version - is emitted later, and the row count that reveals a conflict only exists once that statement has run. So the conflict is discovered at **write time**, which in a deferring layer is not the time the code looked like it was writing. ## Where the write time actually falls Three moments commonly emit accumulated changes: - **An explicit flush**, requested by the code so pending changes hit the database now. - **An automatic flush before a query**, done by layers that keep queries consistent with pending in-memory changes: rather than let a query return rows that contradict them, the layer writes first. - **The commit** at the end of the unit of work, which flushes anything still pending and then commits. Some layers do not defer at all and send each write as it is requested; there the conflict lands on the calling line. Layers differ here, and a candidate who only knows one style is often surprised by the other. What does not differ is the rule: the conflict appears where the statement appears. ## Why commit-time discovery is awkward When the unit is committed by an outer boundary - a framework-managed transaction around a service method, for example - the failure is thrown *after* the method body has returned. Consequences: 1. A handler written around the body never sees it. The error surfaces further out, often as a generic failure at the entry point. 2. Work the method did before the write may already have escaped the transaction: a message sent, a file written, a call made to another system. Rolling back the rows does not undo those. 3. The stack trace points at the boundary, not at the code whose change lost, which makes the cause harder to read from logs alone. ## Failing early on purpose When a use case genuinely needs to react to a conflict itself - to report a precise message, to choose between two courses of action, to avoid an expensive later step - force the write to happen at a point of your choosing: | Where the check runs | You can still... | Watch out for | |---|---|---| | Explicit flush mid-unit | catch it in the same method, before later side effects | the unit is still open, and the changes are not yet durable | | Automatic flush before a query | nothing deliberate - it just happens | conflicts appearing from an innocent-looking read | | Commit at the boundary | only report from the entry point | side effects that already ran, and a distant stack | Flushing early does not make anything durable - only the commit does that - it just moves the *detection* earlier. ## What the unit is worth afterwards Once a guarded write has failed, the objects in memory no longer describe any row that exists, and the unit is finished as far as useful work goes. Whatever the code does next, it starts by ending that unit and reading the current state fresh. Continuing to make changes on the same tracked objects and hoping the next flush behaves is the mistake to avoid. ## Two side effects worth ordering deliberately Because detection is late, anything that must not happen when the write loses should be scheduled to run **after** the commit, not in the middle of the method. Conversely, anything the conflict decision depends on should happen **before** the flush you force. Getting this ordering right is usually a bigger win than any cleverness in the handler itself: the failure is rare, and the damage it does is mostly the irreversible work that ran before anyone knew it had lost. ## Reading it in production Conflicts that arrive at commit tend to show up in logs as failures of the outermost operation, with no mention of the record involved. If they matter operationally, capture the identity of the row and the versions involved where the conflict is *raised*, rather than trying to reconstruct it from the boundary. That is the difference between knowing that two per cent of edits collide and knowing which screen and which record they collide on.

  • Why can a plain read trigger a version conflict in some layers?
    Because a query that must agree with pending in-memory changes forces a flush first, and the flush emits the guarded update. The conflict then surfaces on a line that only looks like a read, which is one reason to know whether your layer flushes automatically before queries.
  • Does forcing an early flush make the change durable, so the conflict cannot happen later?
    No. A flush sends statements inside the still-open transaction; nothing is durable until commit, and the transaction can still roll back afterwards. What the early flush buys is earlier detection, in a place where the code can still respond.

saying these in an interview costs you the question

  • Thinks the conflict is raised when the field is assigned
  • Wraps the service body in a handler and assumes commit failures land there
  • Treats a flush as a commit
  • Sends messages or calls other systems before the guarded write runs
  • Keeps editing tracked objects after a failed flush