skip to content

For an edit that spans several requests — a multi-step wizard, or a form a user keeps open for minutes — how would you decide between carrying a detached object graph across the steps, holding an extended persistence context, and re-reading fresh state each step while applying a recorded change set? What does each choice cost, and what do you owe the user when a conflict is detected?

level: principalimportance: nice to knowfreq 24%

answer

  1. Application transaction ≠ database transaction
  2. Where does state live? How is staleness detected?
  3. Detached = stale + whole-object writes
  4. Change set = one-request staleness, field-scoped writes
  5. Conflict is a product decision, not a retry loop

basics

~20 s

All three are ways to hold an application-level transaction across user think-time. Detached graphs are cheap but stale and destructive on merge; an extended persistence context keeps identity and dirty checking at the cost of server memory and pinned state; a change set re-read each step is the most robust. Whichever you pick, carry a version token and surface conflicts to the user rather than silently resolving them.

solid answer

~60 s

Think of it as an **application transaction** spanning several database transactions, so someone must own staleness. - **Detached graph across steps.** Simple, stateless server, but the state ages the whole time, `merge` writes whole objects, and collection edits become destructive. Acceptable for a small aggregate edited briefly. - **Extended persistence context.** Keeps identity, dirty checking and lazy loading alive across steps, so edits accumulate naturally and flush once. Costs: server-side session state (memory, affinity, cleanup on abandonment) and a context that grows with everything touched. - **Re-read plus change set.** Each step reads current state and applies a small recorded set of intended changes to freshly loaded entities. The staleness window shrinks to one request, writes are field-scoped, and steps are naturally idempotent — at the cost of writing the change-set model. My default is the third for anything long-lived or multi-user, with a version token compared on commit. On conflict, do not silently retry or overwrite: reload, show what changed and by whom, and let the user re-apply, unless the field is safely mergeable.

code

java · 6 lines
java
@Transactional
void commit(long id, String expectedToken, List<Change> changes) {
    Order order = em.find(Order.class, id);
    if (!expectedToken.equals(tokenOf(order))) throw new ConflictException(diff(order));
    for (Change c : changes) c.applyTo(order);   // only intended fields move
}   // dirty checking emits the minimal UPDATE; version guards the commit

go deeper

for a junior

Recognise that a long edit cannot be one database transaction and that a version value is needed to detect changes made in between.

for a middle

Compare the options concretely — what merging a detached graph writes, what an extended context keeps alive — and know the token must be captured at the start.

for a senior

Argue a default with reasons, cover abandonment, timeouts and the extended context's memory and affinity costs, and design an explicit conflict-handling path.

for a principal

Own the staleness model as an architectural decision: conflict granularity, product-level resolution policy per data type, replayability of steps, and the invariants that every team must hold regardless of the chosen mechanism.

## Frame the problem correctly A user editing for minutes is a business transaction that cannot be a database transaction — holding one open across think-time pins connections, holds locks, and collapses throughput. So the work spans several short database transactions, and the state in between lives somewhere in the application. Every option is a choice about **where that state lives and how staleness is detected**, and the interview answer that scores is the one that names those two axes before naming technologies. ## Option 1: carry a detached graph The server stays stateless: load once, hand the object to the client (or a session store), take it back, merge. *Strengths.* No server memory per user; trivially horizontally scalable; the client already needs the data to render. *Costs.* The snapshot ages for the whole conversation, so the conflict window is the entire editing session. `merge` writes whole objects, so untouched fields carry stale values back; collections behave as full replacements with orphan deletion; the identifier and version become client-controlled inputs that must be treated as untrusted; and the payload grows with the aggregate. *When it is fine.* Small aggregates, one editor in practice, short sessions, and a version token that genuinely round-trips. ## Option 2: extended persistence context Keep one persistence context alive across the steps (`PersistenceContextType.EXTENDED`, or a long Hibernate `Session` held by a conversation-scoped component), joining a transaction only when a step needs to write, and flushing at the end. *Strengths.* Entities stay managed, so identity is stable, dirty checking accumulates edits without any merge, lazy loading still works between steps, and the final flush writes exactly what changed — no whole-object overwrite, no collection surprises. Manual flush mode makes intermediate steps non-writing by construction. *Costs.* Real server-side state: memory proportional to everything touched, session affinity or a shared store, timeouts and cleanup for abandoned conversations, and a context that only grows (no `clear()` without losing the point). The first-level cache also goes stale in its own way — it happily serves entities read at step one at step five. And the model does not survive a process restart. *When it fits.* Rich desktop-like editors, wizards over a bounded aggregate, single-node or affinity-friendly deployments. ## Option 3: re-read and apply a change set Model what the user is doing as data: a list of intended changes (“set address”, “add line 3”, “remove line 7”), accumulated client-side or in a lightweight store. Each step — and certainly the commit — loads current entities and applies only those changes. *Strengths.* The staleness window is one request. Writes are field-scoped, so unrelated concurrent edits survive. Steps are replayable and naturally idempotent, which matters for retries. It states intent explicitly, so conflict resolution can be per-change rather than per-object, and it composes with auditing and undo. It is also the only option where “absent from the payload” never means “delete”. *Costs.* You design and maintain the change-set vocabulary; validation must run against freshly loaded state; and previewing intermediate results requires applying the changes to a loaded copy. ## The token, in all three Detection needs a version captured when the user began: a `@Version` value, an ETag, or a hash of the fields being edited. Compare it at commit and fail the write when it moved. Without a token, all three degrade to last-writer-wins. Keep the token narrow when you can — an entity-level version means two users editing different fields of the same row collide unnecessarily; field-level or fragment-level tokens reduce false conflicts at the cost of bookkeeping. ## What you owe the user on conflict An optimistic-lock failure in a human workflow is a **product decision**, not an error page. Options, roughly in order of ambition: reject with a clear message and a reload; reload and show a diff of what changed and by whom so the user re-applies; auto-merge non-overlapping fields and ask only about true collisions; or, for additive data such as comments and log lines, accept both. Automatic retry of the same stale state is the one answer that is simply wrong — it reintroduces the lost update you were guarding against. Also design for **abandonment**: drafts expire, extended contexts time out, and locks (if you ever take pessimistic ones for a conversation) must not outlive a browser tab. ## How to close the answer Default to option 3 for multi-user, long-lived, or high-value edits; option 2 for bounded wizards where server state is acceptable; option 1 only for small aggregates with a real version token. Then state the invariants that hold regardless: never accept whole entity graphs as write payloads, always carry a token, never silently resolve a conflict, and make abandonment a first-class case.

  • Why not simply take a pessimistic lock for the duration of the edit so conflicts cannot happen?
    A database-level lock held across user think-time pins a connection and blocks other writers for an unbounded time, and it does not survive an abandoned browser tab. If exclusivity is a genuine business requirement, model it as an application-level checkout with an owner, a timestamp and an expiry stored in a row — visible to users, releasable by an administrator, and independent of any database transaction.
  • How would you reduce false conflicts when two users edit different fields of the same record?
    Narrow the token's granularity: version the fragments users actually edit, or compare the before-values of only the fields a change set touches, rather than a single row-level version. Non-overlapping changes then commit cleanly and only true collisions surface. The cost is bookkeeping and a more complex commit path, so it is worth it only where multi-user editing of wide records is common.

Three ways to edit a shared spec while others work on it: mail yourself a printed copy and retype it later; keep the live document checked out on your desk; or send a list of tracked changes applied against whatever the current version is when you submit.

saying these in an interview costs you the question

  • Proposing one long database transaction across user think-time
  • Retrying an optimistic-lock failure by re-submitting the same stale state
  • Treating an extended persistence context as free — ignoring memory, affinity and abandonment
  • Assuming a row-level version is always the right conflict granularity
  • Presenting conflict handling as purely technical with no user-facing decision

context