If you have ever wondered why your Spring Boot application feels slow under load, even though everything looked fine during development, there is a good chance you are running into the Open Session in View (OSIV) anti-pattern without realizing it.

In this post, we will look at what OSIV really is, why Spring Boot enables it by default, and why it can silently destroy your application’s performance. More importantly, we will see how to fix the root cause instead of hiding the symptoms.

What is Open Session in View?

Open Session in View is a pattern, and today often an anti-pattern, that keeps a Hibernate session open for the entire lifetime of an HTTP request. That means the persistence context remains active not only during your service layer and transaction, but also during view rendering or JSON serialization, after the transaction has already completed.

OSIV does not keep a database connection open the whole time. However, it does allow database queries to be executed at unpredictable moments, often very late in the request lifecycle.

The pattern originated in early server-side rendered applications. Technologies like JSP or Thymeleaf needed access to lazy-loaded associations while rendering HTML views. Keeping the session open allowed developers to navigate object graphs in the view without hitting a LazyInitializationException.

In modern REST APIs, this convenience comes at a high cost.

Spring Boot’s Controversial Default

What surprises many developers is that Spring Boot enables OSIV by default. When you start your application, you will see a warning like this in the logs:

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

Many teams either ignore this warning or do not fully understand what it means. After all, if the framework enables it by default, it must be fine. Right?

The warning exists for a reason. Spring Boot chooses convenience over safety here, but it does not mean OSIV is a good idea for every application.

The Anatomy of the Problem

To understand why OSIV is dangerous, let us look at a realistic domain model. Consider a simple e-commerce system with the following relationships:

Customer (1) ──── (*) PurchaseOrder (1) ──── (*) OrderItem (*) ──── (1) Product

A customer has many orders, each order has many items, and each item references a product.
JPA entities might look like this:

@Entity
public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "customer_seq")
    private Long id;

    private String firstName;
    private String lastName;

    @OneToMany(mappedBy = "customer")
    @OrderBy("orderDate desc")
    private List<PurchaseOrder> orders = new ArrayList<>();
}
@Entity
public class PurchaseOrder {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "purchase_order_seq")
    private Long id;

    private LocalDateTime orderDate;

    @ManyToOne(cascade = CascadeType.ALL, optional = false)
    private Customer customer;

    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "purchase_order_id")
    @OrderBy("id")
    private List<OrderItem> items = new ArrayList<>();
}
@Entity
public class OrderItem {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "order_item_seq")
    private Long id;

    private int quantity;

    @ManyToOne(fetch = FetchType.LAZY)  // This is the critical piece
    private Product product;
}

Notice the critical part: OrderItem.product is lazy-loaded. This is the default for many-to-one associations in JPA and usually a good choice. You do not always need the product when loading an order item.

The N+1 Query Problem in Action

Now look at a typical REST controller:

@RestController
@RequestMapping("orders")
class OrderController {

    private final PurchaseOrderRepository purchaseOrderRepository;
    private final ModelMapper modelMapper;

    @GetMapping("/osiv")
    List<PurchaseOrderDTO> getPurchaseOrders() {
        var purchaseOrders = purchaseOrderRepository.findAll();
        return purchaseOrders.stream()
                .map(order -> modelMapper.map(order, PurchaseOrderDTO.class))
                .toList();
    }
}

The repository is simple:

public interface PurchaseOrderRepository extends JpaRepository<PurchaseOrder, Long> {
}

At first glance, nothing looks wrong.

What Actually Happens

When a client calls GET /orders/osiv, here’s the sequence of events:

  1. Initial Query: findAll() executes a single SELECT on the purchase_order table
  2. Mapping Begins: For each order, ModelMapper converts it to a DTO
  3. Lazy Loading Triggers: As ModelMapper traverses the object graph:
  • Accessing order.customer triggers a query (N queries for N orders)
  • Accessing order.items triggers another query per order
  • Accessing item.product for each item triggers yet more queries

With 100 orders averaging 5 items each, you’re looking at:

  • 1 initial query
  • 100 queries for customers
  • 100 queries for items collections
  • 500 queries for products

That’s 701 queries for what should be a single database round-trip.

Why OSIV Makes This Worse

Here is the dangerous part.

Without OSIV, this code would fail fast. As soon as mapping touches a lazy association outside the transaction boundary, a LazyInitializationException would be thrown. You would be forced to fix the data access problem immediately.

With OSIV enabled, the session remains open during JSON serialization. Lazy loading works, the endpoint returns correct data, and everything appears fine.

The performance problem only shows up under load, often in production.
OSIV hides architectural issues instead of exposing them.

The Silent Performance Degradation

The N+1 problem is only the most visible issue. OSIV introduces several deeper problems.

Unpredictable Query Execution

Queries can execute anywhere: during serialization, in view rendering, or deep inside mapping libraries. Your service layer metrics may look great while your database is under heavy load.

Accidental Data Exposure

DTO mapping libraries may traverse associations you never intended to expose. This can lead to leaking sensitive data or triggering massive additional queries without anyone noticing.

The Solution: Explicit Fetch Joins

The solution is simple and explicit: tell JPA exactly what data you need.

public interface PurchaseOrderRepository extends JpaRepository<PurchaseOrder, Long> {

    @Query("""
        select o from PurchaseOrder o
        join fetch o.items i
        join fetch o.customer c
        join fetch i.product
        """)
    List<PurchaseOrder> findAllFetchRelations();
}

This query loads everything in one go.

Important note: fetch joins must be used carefully. They can cause Cartesian products and do not work well with pagination. Use them where they make sense and keep queries purpose-specific.

SELECT o.*, i.*, c.*, p.*
FROM purchase_order o
JOIN order_item i ON i.purchase_order_id = o.id
JOIN customer c ON o.customer_id = c.id
JOIN product p ON i.product_id = p.id

Your controller now becomes safe and predictable:

@GetMapping("/fetch")
List<PurchaseOrderDTO> getPurchaseOrdersFetchRelations() {
    var purchaseOrders = purchaseOrderRepository.findAllFetchRelations();
    return purchaseOrders.stream()
            .map(order -> modelMapper.map(order, PurchaseOrderDTO.class))
            .toList();
}

Result: one query instead of hundreds.

Disabling OSIV

Once you’ve fixed your data access patterns, disable OSIV in your application.properties:

spring.jpa.open-in-view=false

This provides several benefits:

  1. Fail-fast behavior: Lazy loading outside transactions throws exceptions immediately
  2. Predictable performance: All queries execute within well-defined service boundaries
  3. Efficient connection usage: Database connections are released as soon as the transaction completes
  4. Clear architecture: Forces a clean separation between data access and presentation

Best Practices for Life Without OSIV

1. Use DTOs at Service Boundaries

Mapping should happen inside transactional service methods, because mapping itself may touch associations.

@Service
@Transactional(readOnly = true)
public class OrderService {

    public List<OrderDTO> getAllOrders() {
        return repository.findAllFetchRelations()
                .stream()
                .map(this::toDTO)
                .toList();
    }
}

2. Create Purpose-Specific Queries

Different use cases need different data. Create repository methods that fetch exactly what each endpoint needs:

public interface PurchaseOrderRepository extends JpaRepository<PurchaseOrder, Long> {

    // For the order list view (minimal data)
    @Query("select o from PurchaseOrder o join fetch o.customer")
    List<PurchaseOrder> findAllForList();

    // For the order detail view (full data)
    @Query("""
        select o from PurchaseOrder o
        join fetch o.items i
        join fetch o.customer
        join fetch i.product
        where o.id = :id
        """)
    Optional<PurchaseOrder> findByIdWithDetails(@Param("id") Long id);
}

3. Consider Entity Graphs

For complex scenarios, JPA Entity Graphs offer a declarative alternative to JPQL fetch joins:

@EntityGraph(attributePaths = {"items", "items.product", "customer"})
List<PurchaseOrder> findAll();

4. Monitor Your Queries

Use tools like:

  • Hibernate Statistics:
    Enable with spring.jpa.properties.hibernate.generate_statistics=true
    Add the logger for Hibernate >= 7
    logging.level.org.hibernate.session.metrics=debug
    or Hibernate < 7
    logging.level.org.hibernate.stat=debug
  • p6spy: SQL logging with timing information
  • Hypersistence Optimizer: Detects JPA performance issues at development time

Measuring the Impact

To truly appreciate the difference, let’s look at some numbers from a test dataset with 1,000 orders, each having 3-5 items:

EndpointQueries ExecutedResponse Time
/orders/osiv4,950~1,200ms
/orders/fetch1~240ms

The reposone is ~1 second faster. That’s a 5x improvement in response time from a single query optimization.

Conclusion

Open Session in View is a well-intentioned pattern that solves a real problem, but it does so by hiding symptoms rather than addressing root causes. In modern applications, especially RESTful APIs, the costs far outweigh the convenience.

The path forward is clear:

  1. Disable OSIV in your Spring Boot applications
  2. Use explicit fetch joins to load the data you need
  3. Map to DTOs within transactional boundaries
  4. Monitor your queries to catch N+1 problems early

YYour database, your operations team, and your users will all benefit.

The code examples in this post are from a demonstration project available at https://github.com/simasch/open-session-in-view.
Feel free to clone it and experiment with the difference yourself using ./mvnw spring-boot:test-run.