Spring Boot ships with one line of YAML you have probably never set, and a single WARN message at startup that almost nobody reads. The line is spring.jpa.open-in-view. Its default is true. That default is the quietest, most expensive architectural decision the framework makes on your behalf, and almost every Spring Boot service in production has it left as is.
Turning it off is a one-line change. The interesting work is in the consequences.
What it actually does
Open Session in View, OSIV for short, is a request-scoped interceptor. Spring registers an OpenEntityManagerInViewInterceptor that opens a Hibernate Session at the start of an HTTP request and closes it when the response is committed. The session lives across every @Transactional boundary inside that request. Code that runs after your service method returns, including the controller layer, Jackson serialization, and Thymeleaf rendering, still has a session attached.
That is why lazy-loaded associations work when you touch them from a controller method or a view template. The proxy on order.getItems() finds an open session, fires the SQL, and returns. With OSIV off, that same call throws LazyInitializationException.
Why it is on by default
This is Spring 1.x era convenience. Server-rendered apps. The user pattern was load an entity in a controller, hand it to a JSP, let the JSP traverse whatever it needs. Lazy initialization made the model object cheap to load up front; OSIV made the rendering not blow up. Spring Boot inherited the default and never broke compatibility.
The Spring team is openly conflicted about it. Reference documentation calls OSIV "controversial" and says the recommendation is to turn it off. Yet the default stays for historical reasons. Since Spring Boot 2.0, leaving the default produces this log line at startup:
spring.jpa.open-in-view is enabled by default. So database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning.If you have never seen that line, search your logs. It is there.
The connection-pool tax
The session holds a JDBC connection for the entire HTTP request, including the parts of the request that are not doing any database work. That includes JSON serialization, response compression, and the bytes-on-the-wire phase where the client is still draining the body.
Concretely: a service with a HikariCP pool of 10 connections and a 200ms average response time, where the actual SQL portion is 30ms, caps at roughly 50 requests per second with OSIV on. The connection is locked for 200ms but only used for 30. Turn OSIV off and the connection is released the moment the service method commits. Same pool, same hardware, now serves five to six times the traffic before queuing.
This is the failure mode that produces the 9am-incident shape: a baseline that works fine, then a moderate spike, then queue buildup as connections back up, then 503s. Pool sizing is not the issue. Each connection is being held longer than necessary.
The architectural rot
The performance cost is the easy half. The harder half is what OSIV does to how you write code.
With OSIV on, lazy traversal works from anywhere in the request. A new developer reads a controller method, sees this:
@GetMapping("/orders/{id}")
public OrderResponse get(@PathVariable Long id) {
Order order = orderService.find(id);
return new OrderResponse(
order.getId(),
order.getCustomer().getEmail(),
order.getItems().stream().map(ItemResponse::from).toList()
);
}There are at least two lazy loads here: order.getCustomer() and order.getItems(). Each may be a separate SQL query, fired from the controller layer, in code that does not import a single repository class. The developer reads the method, decides it looks clean, and moves on.
With OSIV off, the second the controller touches order.getCustomer() outside the service transaction, you get an exception. Now the same code looks like this:
@GetMapping("/orders/{id}")
public OrderResponse get(@PathVariable Long id) {
Order order = orderService.findWithCustomerAndItems(id);
return OrderResponse.from(order);
}The fetching is the service's responsibility. The controller is just the HTTP adapter. The N+1 risk has been pulled into the service layer where you can write a single JOIN FETCH or an @EntityGraph and cover it with a query-count test.
How to know if you have it on
Three signals.
Check spring.jpa.open-in-view in your application.yml. If it is missing, the default is true. If it is explicitly true, the default is also true. Same thing.
Grep the startup logs for the WARN message. If you find open-in-view there, OSIV is on.
Watch for controller methods that touch a lazy association without explicit fetching and somehow do not throw. That is OSIV doing the work silently.
Turn it off
spring:
jpa:
open-in-view: falseRestart. Now run your integration test suite. Several tests probably break with LazyInitializationException. Do not roll back. Each one is a real architectural seam that OSIV was hiding. Fix them one at a time:
For collection associations, use JOIN FETCH in JPQL or @EntityGraph on the repository method:
@EntityGraph(attributePaths = {"customer", "items"})
Optional<Order> findById(Long id);For DTO-shaped responses, project directly with a constructor expression or an interface projection. That skips the entity entirely and you never have a lazy proxy to traverse.
For places that genuinely need a hydrated entity in the controller, return it from a service method that does the loading inside its own @Transactional. The controller stays dumb.
When OSIV is actually fine
Prototypes that exist for two weeks. Internal admin UIs with single-digit RPS. Hackathon code. Anywhere request volume is bounded by humans clicking buttons and the connection pool is over-provisioned. In all of those, OSIV trades a tiny architectural smell for a real ergonomics win, and the trade is correct.
Anywhere with real concurrency, a customer-facing API, or a team larger than three: turn it off. The smell becomes a problem.
What it costs to flip the switch
One line of YAML. Half a day to fix the integration tests that surface. A second half-day to write query-count assertions so the new fetch strategies do not silently regress into N+1s. After that, every controller in the codebase becomes easier to reason about, the connection pool serves more traffic, and the WARN line disappears from your logs.
This is one of the cheapest performance and architecture wins available in a Spring Boot service. The reason it goes unfixed is that the default is invisible until it bites you in production, and by then you are debugging a connection-pool incident instead of reading a tutorial about defaults.
Comments (0)