Skip to main content
the lies your orm tells you

Open-in-View: The Silent Connection Killer

5 min read Chapter 17 of 30

Open-in-View: The Silent Connection Killer

Spring Boot enables spring.jpa.open-in-view=true by default. This single configuration property can be the difference between a connection pool that breathes and one that suffocates.

The Lie

Open-in-view prevents LazyInitializationException. Your entities stay usable throughout the request lifecycle, including during view rendering. Convenience without cost.

The Reality

Open-in-view keeps the Hibernate Session (and its database connection) open for the entire HTTP request lifecycle, from controller entry to response serialization. This means lazy-loaded associations can trigger SQL queries during JSON serialization, during template rendering, and during any downstream processing that touches an entity field.

The connection lifecycle with open-in-view:

  1. HTTP request arrives
  2. OpenEntityManagerInViewInterceptor opens a Session and binds it to the thread
  3. Controller method executes (may or may not use @Transactional)
  4. Controller returns a response object (or model for template)
  5. Response serialization begins (Jackson for JSON, Thymeleaf for HTML)
  6. Serialization accesses lazy fields, triggering SELECT queries outside any transaction
  7. Response is written to the client
  8. OpenEntityManagerInViewInterceptor closes the Session and releases the connection

The connection is held from step 2 to step 8. If response serialization takes 50ms, the connection is held for 50ms of non-transactional, read-only, possibly N+1 query work. If the client is slow to receive the response (slow network, mobile client), the connection may be held even longer depending on server buffering.

The Evidence

@RestController
public class OrderController {

    @Autowired
    private OrderService orderService;

    @GetMapping("/orders/{id}")
    public OrderDTO getOrder(@PathVariable Long id) {
        // Service method is @Transactional(readOnly = true)
        // Transaction commits when findById returns
        Order order = orderService.findById(id);

        // Transaction is over. But the Session is still open (OSIV).
        // Jackson serializes the response. If Order has:
        //   @OneToMany(fetch = FetchType.LAZY)
        //   private List<OrderItem> items;
        //
        // Jackson calls order.getItems(), which triggers:
        // select * from order_items where order_id = ?
        //
        // This query runs OUTSIDE the @Transactional boundary.
        // It runs in auto-commit mode.
        // The connection is held until serialization completes.

        return OrderDTO.from(order);
    }
}

// Timeline:
// T+0ms:    Connection acquired (OSIV interceptor)
// T+1ms:    Transaction begins (@Transactional)
// T+3ms:    SELECT order (inside transaction)
// T+4ms:    Transaction commits, but connection NOT released
// T+5ms:    Jackson starts serializing
// T+6ms:    Jackson hits lazy collection: SELECT order_items
// T+7ms:    Jackson hits nested lazy: SELECT product for each item (N+1!)
// T+50ms:   Serialization complete
// T+51ms:   Connection released (OSIV interceptor)
//
// Connection hold time: 51ms
// Time inside transaction: 3ms
// Time in auto-commit lazy loading: 44ms

The queries at T+6ms and T+7ms run without transaction isolation. On PostgreSQL, they use the default READ COMMITTED level in auto-commit mode, so each query sees a potentially different snapshot of the database. If another transaction modifies order_items between your two queries, you get an inconsistent view.

The Fix

Disable open-in-view:

spring:
  jpa:
    open-in-view: false

Spring Boot logs a warning at startup when open-in-view is enabled:

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

After disabling, you will get LazyInitializationException wherever your code accesses lazy associations outside a transaction. This is a feature. Each exception reveals a place where your code was silently generating queries you did not ask for.

Fix each LazyInitializationException by loading the data you need inside the transactional boundary:

// BETTER: Load what you need in the service layer
@Service
public class OrderService {

    @Transactional(readOnly = true)
    public OrderDTO findOrderWithItems(Long id) {
        Order order = orderRepository.findById(id)
            .orElseThrow(() -> new OrderNotFoundException(id));

        // Force initialization inside the transaction
        // Option 1: Use a fetch join in the repository
        // Option 2: Use EntityGraph
        // Option 3: Initialize explicitly
        Hibernate.initialize(order.getItems());

        // Build the DTO inside the transaction where data is available
        return OrderDTO.from(order);
    }
}

// Or use a repository with fetch join:
public interface OrderRepository extends JpaRepository<Order, Long> {

    @Query("SELECT o FROM Order o LEFT JOIN FETCH o.items WHERE o.id = :id")
    Optional<Order> findByIdWithItems(@Param("id") Long id);
}

The second approach is better. Fetch joins load everything in one query. Hibernate.initialize() triggers a separate query, which is equivalent to the OSIV behavior but at least it runs inside your transaction.

The Cost Model

Open-in-view costs:

  • Connection hold time: Typically 2x-10x longer than necessary. A request that needs 5ms of database work holds a connection for 50-500ms.
  • Connection pool pressure: At 100 concurrent requests with 50ms average hold time (OSIV) vs 5ms (no OSIV), you need 10x fewer connections without OSIV.
  • Consistency risk: Queries outside a transaction boundary see different database snapshots. Rare in practice, but when it happens, the bugs are subtle.
  • Hidden N+1 queries: OSIV masks N+1 problems. Disabling it surfaces them immediately. This is pain up front that prevents production outages later.

For applications with fewer than 10 concurrent users, OSIV overhead is negligible. For applications with 100+ concurrent users and lazy associations on serialized entities, OSIV is a connection pool bottleneck. Disable it early. Fix the LazyInitializationExceptions while the codebase is manageable.