Why Open Session in View (OSIV) Can Kill Your Spring Boot Performance
Spring Boot’s default Open Session in View (OSIV) keeps Hibernate sessions open throughout the request, silently triggering lazy‑loaded queries during JSON serialization, leading to N+1 problems and massive DB load under load, but disabling it and using explicit fetch joins restores performance.
What is Open Session in View (OSIV)?
OSIV is a design pattern – often considered an anti‑pattern today – that keeps the Hibernate Session open for the entire HTTP request lifecycle. After the transaction has finished and the service method has returned, the persistence context remains alive, allowing lazy‑loaded entities to be fetched during JSON serialization or view rendering.
Why OSIV Is Dangerous in Modern REST APIs
OSIV does not hold a database connection continuously, but it permits SQL execution at any unpredictable moment. In traditional server‑side rendered pages (JSP, Thymeleaf) this was convenient, but with REST APIs that return JSON the cost is high. When OSIV is enabled (the default in Spring Boot), lazy loading works silently, the API returns correct data, and the performance problem is hidden until high concurrency or large data volumes cause the database CPU to spike.
Real‑World N+1 Query Example
Typical e‑commerce model:
Customer (1) ── (n) PurchaseOrder (1) ── (n) OrderItem (n) ── (1) ProductJPA entities (simplified):
@Entity
public class Customer {
@OneToMany(mappedBy = "customer")
private List<PurchaseOrder> orders = new ArrayList<>();
}
@Entity
public class PurchaseOrder {
@ManyToOne(cascade = CascadeType.ALL, optional = false)
private Customer customer;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "purchase_order_id")
private List<OrderItem> items = new ArrayList<>();
}
@Entity
public class OrderItem {
@ManyToOne(fetch = FetchType.LAZY)
private Product product;
}REST controller:
@GetMapping("/osiv")
List<PurchaseOrderDTO> getPurchaseOrders() {
var orders = purchaseOrderRepository.findAll();
return orders.stream()
.map(o -> modelMapper.map(o, PurchaseOrderDTO.class))
.toList();
}Repository:
public interface PurchaseOrderRepository extends JpaRepository<PurchaseOrder, Long> {}Execution flow when /osiv is called:
findAll(): 1 SQL to fetch orders.
ModelMapper traverses the object graph.
Lazy loading triggers: order.customer → N SQL order.items → N SQL item.product → N×M SQL
Assuming 100 orders, each with 5 products, the total SQL statements become 701, i.e., more than 700 database round‑trips for a single request.
Correct Approach
Explicitly fetch the needed associations with a single query:
@Query("""
select o from PurchaseOrder o
join fetch o.items i
join fetch o.customer
join fetch i.product
""")
List<PurchaseOrder> findAllFetchRelations();This generates one SQL statement joining all tables, dramatically reducing database load.
Disable OSIV in application.properties:
spring.jpa.open-in-view=falseSummary
OSIV solves the superficial problem of lazy loading in view rendering but hides a deeper architectural flaw. In modern REST API architectures, performance and control must take precedence over the convenience OSIV provides. Disabling OSIV and using explicit fetch joins is the recommended practice.
Reference: https://martinelli.ch/the-hidden-performance-killer-understanding-open-session-in-view-in-spring-boot
Architecture Digest
Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
