The Silent Problem of open-in-view
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:
- Hibernate grabs Connection A from the pool
- Sets
autoCommit=falseand fires the SELECT - The connection stays in “idle in transaction” state — with an open transaction that nobody committed
- Since the
EntityManageris still alive (thanks toopen-in-view), Connection A stays held
Then, when we reach Phase 2 and the TransactionTemplate kicks in:
- Spring requests a new connection from the pool (there’s no Spring-managed transaction, so it doesn’t reuse A)
- HikariCP gives it Connection B
- The transactional hooks do writes (
saveAllAndFlush()) - 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=true | open-in-view=false | |
|---|---|---|
| Connection | Held for the entire request | Released with the transaction |
| Lazy loading outside tx | Works (silent N+1) | LazyInitializationException |
| Connection pool | Higher consumption | Lower consumption |
| N+1 queries | Generated silently | Fail 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.