Introduction
A production incident often starts with something that looks harmless. A developer ships a Spring Boot API to “Return all customers with their orders.” The code is simple, the response looks correct and everything works perfectly in development. Then traffic grows and the same endpoint causes:
- Database CPU spikes
- Connection pool exhaustion
- Higher API latency
- Increasing infrastructure cost
A request that should run a few queries may silently run hundreds or thousands.
Returning 1,000 customers with orders can mean:
1 customer query + 1,000 order queries = 1,001 SQL statements
Locally, with 10 customers, nobody notices.
In production, round-trip cost dominates.

N+1 query explosion
Hibernate is usually not doing something wrong. Hibernate is following:
- Entity relationships
- Lazy loading rules
- Object navigation patterns
The actual problem is a mismatch between Object-oriented programming access patterns and Efficient database access patterns
What Is the N+1 Query Problem?
The N+1 query problem occurs when:
- One query loads the parent records.
- Additional queries load related records for each parent.
The formula is: 1 Parent Query + N Child Queries = N+1 Queries

Customer and Order Relationship
Expected Efficient SQL Approach

Intended one-query shape for customers with orders
Lazy loading often produces chatty SQL instead

Lazy Loading SQL Pattern
Query Growth Example
Customers → SQL Queries
10 → 11 queries
100 → 101 queries
1,000 → 1,001 queries
10,000 → 10,001 queries
The problem is not only the number of rows.The real problem is:
- Database network round trips
- Connection pool occupation
- Database CPU usage
- Increased request latency
Even simple queries become expensive when executed repeatedly.
Hibernate Example
Consider a typical Customer and Order relationship. A customer contains multiple orders.

Customer Entity Mapping Diagram
The orders are not loaded immediately. Hibernate waits until the application accesses:
customer.getOrders();
ManyToOne Lazy Configuration
JPA defaults: @ManyToOne to FetchType.EAGER
This default is often unsuitable for production systems.

Order Entity Code Screenshot
N+1 Query Example
Hibernate may look like object navigation in Java, but every lazy collection access can trigger another SQL query.

N+1 Loop Execution Diagram
How Hibernate Creates N+1 Internally
With LAZY, Hibernate returns a proxy (many-to-one/one-to-one) or a persistent collection wrapper.
SQL runs on first access not when the parent entity is loaded.

Lazy Proxy and Collection Wrappers

Lazy Proxy and Collection Wrappers

Per-Customer Lazy Access in a Loop
Common triggers include association access in loops, Jackson serialization while a session is open and OSIV.
Many enterprise APIs disable OSIV (spring.jpa.open-in-view=false) and enforce explicit loading inside @Transactional services.
Session open → possible N+1 during serialization.
Session closed → often LazyInitializationException.
Why Does N+1 Happen?
1. Lazy Loading Without a Fetch Plan
LAZY is usually the correct default. Blaming LAZY alone is inaccurate.
A customer may own:
- Orders
- Payments
- Addresses
- Transactions
The real bug is fetching data without a plan

Wide Customer Aggregate Graph
2. Entity Relationships
Any association can introduce N+1:
- OneToMany
- ManyToOne
- ManyToMany
ManyToMany can amplify cost because join-table access plus entity loads increase the number of SQL statements.

Relationship Patterns That Trigger N+1
3. Returning Entities from REST APIs
Returning JPA entities from controllers is a frequent production mistake. The repository may look cheap while serialization walks lazy associations.

Entity Controller Anti-Pattern

Jackson Serialization Triggering Lazy SQL

Entity-to-DTO API Boundary
Repository cost is not API cost.
Entities model persistence.
DTOs model contracts.
LAZY vs EAGER
Making everything EAGER usually creates another problem. EAGER does not reliably fix N+1 and does not guarantee one optimal SQL plan—Hibernate may join or issue follow-up selects.
It also over-fetches across unrelated use cases.
LAZY Loading:
→ OneToMany
→ ManyToMany
EAGER Loading:
→ ManyToOne
→ OneToOne

Explicit ManyToOne LAZY Declaration
Therefore explicitly declaring: @ManyToOne(fetch = FetchType.LAZY) is a common production practice.
Architectural rule: Mappings define relationships, use cases define fetch plans.
How To Detect N+1
Latency, CPU and memory alone are not enough.
Ask: How many queries did a request execute?
Treat query count as a non-functional requirement for critical APIs similar to latency and throughput.

Spring Boot Hibernate SQL Logging Config

Repeated Orders SQL Indicating N+1
Avoid permanent verbose SQL logging in production because of volume and overhead.

Enable Hibernate Statistics

CI Query Budget Failure Example

Request-to-Database Observability Flow
Also index join FKs such as:
orders.customer_id
Hibernate cannot fix missing indexes.
Use:
- p6spy
- datasource-proxy
- OpenTelemetry
- APM tools
as appropriate and test with production-like cardinality.
Solutions
There is no universal fix. Choose based on:
- Data volume
- API requirements
- Transaction boundaries
- Read/write patterns
1. JOIN FETCH
JOIN FETCH Repository Query — Demonstrates explicit fetch joining of orders with customers.

JOIN FETCH Repository Query

SQL Generated by JOIN FETCH
Use JOIN FETCH for:
- Bounded parent + one collection
- Cases where managed entities are required
Avoid when:
- Paginating joined collections
- Fetching multiple List bags

JOIN FETCH Pagination Mismatch
SQL may return duplicate customer rows because of the join.
Hibernate can remove duplicate entity references, but the database still processes the larger result set.
Prefer DISTINCT for the result list and measure join-row volume.

Duplicate Parent Rows from Join

Cartesian Product from Multiple Collection Joins
2. EntityGraph
Spring Data EntityGraph Example — Shows repository-level fetch planning while mappings stay LAZY.

Spring Data EntityGraph Example
Use EntityGraph for different repository graphs without duplicating JPQL.
Prefer DTOs when:
- The API needs only a few columns
- The entity is wide
3. DTO Projection (Often Best for APIs)
Most list APIs do not need full entity graphs.

Minimal API JSON Response

CustomerOrderDTO Definition

DTO Constructor Projection Query
Benefits:
- Smaller SQL
- Less memory usage
- Faster serialization
- No accidental lazy loads
For heavy reads, consider dedicated read models or CQRS-style projections.
4. Batch Fetching
Batch fetching reduces lazy-loading chatter with minimal mapping changes.

@BatchSize Collection Mapping

Global Batch Fetch Size Property

Batch Fetch vs Per-Parent Lazy Queries
Excellent legacy mitigation—not always the long-term design for hot APIs.
5. FetchMode.SUBSELECT
Useful for broad in-session graphs, weaker for precise API projections. Second-level cache is not an N+1 strategy—it does not replace fetch planning.

FetchMode.SUBSELECT Mapping

SUBSELECT Collection SQL
Production Challenges
MultipleBagFetchException: fetching multiple List collections with JOIN FETCH can fail. Prefer separate queries, DTOs or batching. Switching to Set may avoid the exception, not cartesian cost.
For paged APIs with child data avoid JOIN Orders LIMIT 20. Use a two-step pattern:

Two-Step Pagination with Child Data
The same anti-pattern appears across services:

Database N+1 vs Microservice N+1
Use bulk APIs, batching, caching, or read models. Avoid chatty communication.
Decision Guide
1. JOIN FETCH
Use when: You need a bounded parent + one collection and managed entities.
Avoid when: Paginating joined collections or fetching multiple List bags.
Why: It reduces round trips, but joins can multiply result rows and create pagination or cartesian-product problems.
2. DTO Projection
Use when: Building REST/read APIs that need only specific fields.
Avoid when: You need fully managed entities for further in-place changes.
Why: It keeps SQL narrow, reduces memory usage, improves serialization and prevents accidental lazy loading.
3. EntityGraph
Use when: Different repository methods need different fetch graphs while entity mappings remain LAZY.
Avoid when: The use case is better represented by a narrow DTO projection.
Why: It lets the repository define the fetch plan without duplicating JPQL.
4. Batch Fetching / SUBSELECT
Use when: You need a low-change mitigation for existing lazy-loading patterns.
Avoid when: A hot API path requires precisely optimized SQL.
Why: It reduces N+1 query chatter but does not replace deliberate fetch planning.
5. Separate Queries / DTOs
Use when: The API needs multiple collections or paginated parent/child data.
Avoid when: You can safely satisfy the use case with a single bounded fetch.
Why: Separate queries prevent multi-collection JOIN FETCH problems and give you better control over result-set size.
Best Practices
- Keep associations LAZY by default, explicitly set @ManyToOne(fetch = LAZY) when appropriate.
- Define fetch plans per use case, return DTOs from API boundaries.
- Treat query count as an NFR for critical APIs, enforce CI query budgets.
- Index FK/join columns, load-test with production-like data volumes.
- Prefer disabling OSIV and loading inside transactional service boundaries.
- Design bulk service APIs to prevent distributed HTTP N+1.
Conclusion
N+1 is a database communication design problem exposed by ORM convenience. Choose JOIN FETCH, EntityGraph, DTOs, batching/SUBSELECT or read models by use case—not by habit. Do not let entity navigation silently decide database traffic. Design fetch strategies intentionally and prove them with SQL and query-count gates.