Back to articles
10 min read

The Silent Problem of open-in-view

Alejandro Alonso Noguerales

Alejandro Alonso Noguerales

Apr 9, 2026

You’ve probably seen this warning. It shows up every time you start Spring Boot and everyone ignores it:

WARN: spring.jpa.open-in-view is enabled by default.
Therefore, database queries may be performed during view rendering.
Explicitly configure spring.jpa.open-in-view to disable this warning.

We ignored it too. We were building a new API with Spring Boot 4 and virtual threads. Everything was going well, tests were passing, development was moving along smoothly.

Until one day the test suite started hanging. No errors. No exceptions. No timeouts. It just sat there, in silence, waiting for something that was never going to come.


01 — The context

We have a Spring Boot 4 API with a hook-based CRUD architecture. The update flow has three phases:

Phase 1 (no transaction):  findById() + pre-hooks (validations, external calls)
Phase 2 (with transaction): TransactionTemplate { save + transactional hooks }
Phase 3 (no transaction):  post-hooks (notifications, events)

The reasoning behind this design is to avoid keeping a database transaction open while running validations or calling external services. Only the actual write goes inside the TransactionTemplate. It’s a pattern we had been using for months without issues.

02 — What happened

We launched the full suite of over 10,000 tests and the execution hung on an update test. The weird part: individual tests passed just fine. Filtering by -Dgroups=e2e also worked. But running everything together hung at the same point every time.

03 — Finding the cause

First attempt: Spring contexts

The first thing we noticed was that we had two base test classes (AbstractWebMvcTest and AbstractSecureWebMvcTest), each with its own @DynamicPropertySource. In Spring Test, that means two different Spring contexts, each with its own HikariCP connection pool, both hitting the same Testcontainers PostgreSQL.

We unified the contexts so everything went through a single HikariPool. It still hung. False lead — although the fix was worth it anyway, because every extra Spring context in tests means an extra HikariPool, and with small test pools (maximum-pool-size: 5) you run out of connections fast.

Second attempt: looking at what was actually happening

We took a thread dump and checked pg_stat_activity in PostgreSQL. There it was:

Main thread:     BLOCKED on CompletableFuture.get()
Virtual thread:  BLOCKED on PGStream.receiveChar() during saveAndFlush()
PID 168 | idle in transaction | SELECT ... FROM communication WHERE ...  | 8 min
PID 169 | active              | UPDATE communication SET ...             | 8 min | wait: Lock/transactionid

Two connections from the same pool. From the same request. Waiting for each other.

04 — The root cause

With open-in-view=true (Spring Boot’s default), Hibernate keeps the EntityManager open for the entire HTTP request. The consequences aren’t obvious until they blow up in your face.

When our findById() runs in Phase 1, outside any Spring transaction:

  1. Hibernate grabs Connection A from the pool
  2. Sets autoCommit=false and fires the SELECT
  3. The connection stays in “idle in transaction” state — with an open transaction that nobody committed
  4. Since the EntityManager is still alive (thanks to open-in-view), Connection A stays held

Then, when we reach Phase 2 and the TransactionTemplate kicks in:

  1. Spring requests a new connection from the pool (there’s no Spring-managed transaction, so it doesn’t reuse A)
  2. HikariCP gives it Connection B
  3. The transactional hooks do writes (saveAllAndFlush())
  4. Connection B tries to UPDATE rows that Connection A has locked with its phantom transaction
Connection A (idle in transaction):  SELECT ... FROM entity WHERE ...  [implicit open transaction]
Connection B (active):               UPDATE entity SET ...             [waiting for A to commit]

B waits for A to commit. A won’t commit until the request ends. The request won’t end until B completes. Classic deadlock.

What about virtual threads?

With platform threads you might get away with it because the thread pool is usually small (200 threads), the connection pool is sized proportionally, and the window for this scenario is very narrow.

With virtual threads you can have thousands of concurrent requests, each holding its connection through the EntityManager, against a pool of 10-50 connections. The probability of exhausting the pool and triggering the deadlock goes way up. We caught it in tests. If it had reached production undetected, it would have been much harder to diagnose.

05 — How we fixed it

The obvious part: disabling open-in-view

spring:
  jpa:
    open-in-view: false

The EntityManager closes after each transaction instead of at the end of the request. The findById() in Phase 1 grabs a connection, runs the SELECT, and releases it immediately. When the TransactionTemplate starts, it gets a clean connection. No conflict.

The not-so-obvious part: LazyInitializationException

Once open-in-view is disabled, all entities become detached as soon as the transaction closes. Any access to a lazy relation outside a transaction throws a LazyInitializationException. We had to touch every base CRUD interface — reads, writes, repositories. How we solved it and with what criteria is what we cover in detail in Part 2.

The bug that open-in-view had been hiding since day one

The best part came when auditing the services. We found a service that used the generic update pipeline to modify a specific field on an entity. The problem: the update mapper had a @Mapping(target = "field", ignore = true) for that field. The mapper was silently discarding it.

With open-in-view=true this “worked”. The entity stayed managed throughout the request, Hibernate detected the change via dirty checking and persisted it anyway. With open-in-view=false, the entity was detached and the change was lost.

A bug hiding in plain sight thanks to an implicit mechanism nobody asked for. The fix: a direct repository.saveAndFlush(entity), bypassing the entire generic update pipeline.

This is what’s really dangerous about open-in-view: it doesn’t just hold connections — it masks bugs. Code that looks correct works by accident.

06 — What we learned

open-in-view + virtual threads = deadlock. Spring Boot enables it by default. With virtual threads and programmatic transactions (TransactionTemplate), it becomes a ticking time bomb.

open-in-view=false forces you to be explicit. And that’s a good thing:

open-in-view=trueopen-in-view=false
ConnectionHeld for the entire requestReleased with the transaction
Lazy loading outside txWorks (silent N+1)LazyInitializationException
Connection poolHigher consumptionLower consumption
N+1 queriesGenerated silentlyFail with exception

07 — In summary

If you’re using Spring Boot with virtual threads, put this in your application.yml now:

spring:
  jpa:
    open-in-view: false

You’ll get LazyInitializationException in several places. That’s fine. Each of those exceptions is a spot where your code was running extra queries without you knowing, or relying on an implicit mechanism that under load can hang your application. Your app will be more predictable, consume fewer connections, and won’t mysteriously hang on a Tuesday at 3 AM.

Now comes the hard part: resolving each LazyInitializationException. There are several strategies, and choosing wrong can generate performance problems as serious as the ones you just fixed. That’s exactly what we cover in Part 2.

Spring BootHibernateJPAVirtual ThreadsDeadlock