skip to content

In an application where the persistence context stays open until the HTTP response has been rendered, code running during rendering modifies a managed entity after the service transaction has already committed. What happens to that modification, and why is either possible outcome dangerous?

level: seniorimportance: nice to knowfreq 24%

answer

  1. managed after commit, no transaction behind it
  2. close without flush = silent loss
  3. auto-flush outside tx = auto-commit write
  4. no rollback, no atomicity, no audit
  5. detach or return DTOs at the boundary

basics

~20 s

Usually nothing: no transaction commits afterwards, so the change is discarded silently when the session closes. If automatic flushing is left enabled, a query during rendering can auto-flush it — writing outside any transaction, in auto-commit, with no rollback possible.

solid answer

~50 s

The entity is still managed, so Hibernate is still dirty-tracking it — but nothing will commit. Two outcomes exist, and neither is good. **Silently lost.** The session is closed by the request filter without a flush, so the queued `UPDATE` evaporates. No exception, no log line — the change simply never happened, which is a miserable bug to chase because the code looks correct. **Silently written, untransactionally.** If automatic flushing is active and something during rendering executes a query touching the same table, the auto-flush fires and the `UPDATE` is sent outside any transaction, typically in auto-commit. It commits on its own, cannot be rolled back with the request, and is not atomic with the service-layer work it logically belongs to. A later rendering failure leaves a half-applied change. Because of the second risk, implementations of the pattern typically force the request-scoped session into a non-flushing mode outside transactions, converting the danger into the first, quieter failure.

code

java · 6 lines
java
// service (transactional) returns a still-managed entity
Order order = orderService.find(id);   // tx committed on return

// during rendering, still inside the request-scoped session:
order.setLastViewedAt(Instant.now());  // dirty, but nothing will commit it
// -> discarded at session close, OR auto-flushed in auto-commit

go deeper

for a junior

Know that a change made after the transaction commits is not saved, even though the object still looks like a normal managed entity.

for a middle

Explain both outcomes — discarded at session close, or auto-flushed outside a transaction — and why flush mode decides which.

for a senior

Emphasise the operational danger of an auto-commit write with no rollback and no audit path, and prescribe detaching or returning projections at the boundary.

for a principal

State the layering rule: the presentation layer must not hold writable managed state, and every write has an owning transaction and an explicit call site.

## The setup When the persistence context is scoped to the whole HTTP request, the entities returned by a service method are still **managed** while the view renders. Managed means Hibernate holds a load-time snapshot and will compare it at the next flush. Nothing about that changes when the transaction commits: the transaction and the persistence context are separate lifetimes, and only the transaction ended. So code in the rendering phase — a template calling a setter, a serializer invoking a method with a side effect, a view helper that "normalizes" a field — can dirty a managed entity in a context that no longer has a transaction behind it. ## Outcome 1: the change is discarded The request filter closes the session in its `finally` block. Closing does not flush. The dirty state is thrown away with the context, and nothing reports it. From the outside, an update simply did not take effect, intermittently, depending on which code path touched the entity. This is the *safe* outcome, and it is what most implementations arrange deliberately: the session participating outside a transaction is set to a manual/never flush mode so an automatic flush cannot fire. ## Outcome 2: the change is written outside a transaction If automatic flushing is active, the auto-flush rule still applies: before executing a query whose tables overlap the pending changes, Hibernate flushes. During rendering there are plenty of queries — every lazy initialization is one. So the `UPDATE` can be emitted while no transaction is open, which means the JDBC connection is in auto-commit and the statement **commits by itself**. Why that is worse: - **No atomicity.** The write is not part of the service transaction it belongs to. If rendering then fails and returns a 500, the partial change is already durable. - **No rollback path.** There is nothing to roll back; error handling at the request level cannot undo it. - **Invisible authorship.** The write originates from the presentation layer, so no service-layer code review, audit hook, or transactional listener sees it. Domain events and post-commit callbacks tied to the service transaction never fire for it. - **Ordering surprises.** Each auto-flushed statement commits independently, so multiple such writes during one render are separately durable in whatever order rendering happened to touch things. ## How to reason about it in an interview The strong answer names both outcomes, says which one the framework normally arranges, and then makes the broader point: **the presentation layer should not be able to write.** Both outcomes are symptoms of one design fault — mutable managed entities escaping into a layer that has no transactional contract. A change that matters must be inside the transaction that owns it. ## Prevention - **Don't let managed entities reach the view.** Return immutable projections or DTOs assembled inside the transaction; nothing the view holds is attached, so nothing it does can be flushed. - **Detach explicitly** when entities must be returned as-is, via `em.detach(...)` or `em.clear()` at the transaction boundary; a detached entity's changes are inert. - **Keep the non-transactional session non-flushing**, so the dangerous outcome cannot happen even if something dirties an entity. - **Make writes explicit and transactional.** Anything that must persist belongs in a service method with its own transaction, invoked before the response is produced — not as a side effect of rendering. - **Test for it.** Assert the statement count for a render path contains zero writes, or run the view layer against a read-only connection in tests so a write fails loudly. ## The related trap The same mechanics explain why "just call `merge()` in the controller after the transaction" does not work as people expect: merging into a request-scoped context makes the entity managed again, but without a transaction the copy is either discarded at close or auto-flushed untransactionally. Re-entering a transactional service method is the only correct route.

  • Why do implementations of this pattern usually force a non-flushing mode outside transactions?
    To prevent an automatic flush from writing outside a transaction. Without it, a query issued during rendering can trigger an auto-flush of dirty entities, sending an UPDATE in auto-commit that commits on its own and cannot be rolled back with the request. Forcing manual flushing converts an untransactional write into a discarded change, which is the lesser evil.
  • What is the reliable way to persist something discovered during rendering, such as a last-viewed timestamp?
    Call a transactional service method explicitly, before or independently of rendering, so the write has its own transaction and normal error handling. If it is genuinely a side concern, record it asynchronously from an event rather than mutating a managed entity that happens to still be attached.

saying these in an interview costs you the question

  • Assuming the service transaction 'covers' anything done during rendering
  • Believing entities are detached the moment the transaction commits when the session is request-scoped
  • Expecting an exception when the change is lost — the failure is silent
  • Calling merge() in a controller and assuming it persists
  • Treating an untransactional auto-commit write as harmless because 'the data got saved'

context