Home Java Hibernate LazyInitialization — Proxy Has No Session
Advanced 5 min · September 23, 2026

Hibernate LazyInitialization — Proxy Has No Session

Hibernate LazyInitializationException: proxy touched after session closed.

N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.

Follow
Production
production tested
September 23, 2026
last updated
1,905
articles · all by Naren
Before you start⏱ 14 min
  • JPA entities and mappings
  • Spring @Transactional basics
  • SQL join fundamentals
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • LazyInitializationException means code touched an unloaded lazy association after the Hibernate session closed
  • The session ends at the service's transaction boundary; views, serializers, and later methods find detached proxies
  • Fix by loading inside the transaction (JOIN FETCH, EntityGraph) and mapping to DTOs before returning
  • Open-Session-in-View silences the error while exploding N+1 queries and holding connections — keep it off
  • Never serialize entities to JSON; Jackson traverses proxies and throws or fires hundreds of queries
✦ Definition~90s read
What is Hibernate LazyInitialization Fix?

LazyInitializationException is Hibernate's refusal to load an uninitialized proxy or collection without an open session: could not initialize proxy - no Session. Lazy associations defer their SELECT until first touch; the touch requires the originating session; service boundaries, closed transactions, and serialization contexts end sessions.

Think of a library that fetches books from the warehouse only when you ask (lazy loading).

Any touch afterward — iteration, size, JSON rendering — throws instead of querying.

The machinery centers on proxies and the persistence context. Loaders return entity instances whose lazy fields hold proxy placeholders wired to the session. While attached, touches trigger transparent SQL; once detached (transaction commit, session close, OSIV-off rendering), the wiring dangles.

Jackson serialization, template loops, and cross-method touches are the three classic post-detachment triggers.

Beginners confuse this with missing data because the entity looks populated — ids and eager fields show real values. The gap is precisely the unloaded associations, invisible until touched. A second confusion blames the query: the query is innocent; the fetch plan (what it loaded) plus the boundary (where the session ended) decide everything.

The professional response designs sessions out of the problem: per-use-case fetch plans declared at the repository, service methods owning whole reads transactionally, DTOs as the only boundary-crossing shape, OSIV disabled, and query-count tests proving constant cost. Lazy exceptions then have nowhere to occur — there is no lazy state left where rendering can reach it.

Plain-English First

Think of a library that fetches books from the warehouse only when you ask (lazy loading). The librarian (session) goes home at 5 PM when your request ticket (transaction) closes. At 6 PM you ask for the sequel — no librarian, no book, just an apology slip (the exception). Open-Session-in-View keeps the librarian working overtime for every reader, which burns out the staff (connections) and empties the shelves one trip at a time (N+1).

The page renders halfway and dies: org.hibernate.LazyInitializationException: could not initialize proxy - no Session. The data exists — the query ran, the entity loaded. But one collection was marked lazy, the session closed at the service exit, and the template's innocent loop became a crime scene.

Lazy loading is Hibernate's promise to fetch associations only when touched. That promise needs an open session to run the follow-up SELECT. Service boundaries close sessions; views, serializers, and second methods touch proxies afterward. The proxy finds no session, keeps its promise by throwing, and your page dies rendering row three.

The quick fix — Open-Session-in-View — is the most expensive painkiller in JPA. It holds the session open through rendering, so lazy touches succeed by firing hundreds of surprise queries on connections held far too long. The exception vanishes; the N+1 explosion begins.

This article covers honest fixes: JOIN FETCH for single queries, EntityGraphs for reusable plans, and DTO projections that skip entities entirely. You will learn where transactional boundaries belong, why services must return DTOs, and how query-count tests keep the exception — and its N+1 shadow — gone for good.

Proxies and Detachment: Why Touches Explode After Close

Lazy associations load as proxies: placeholders holding the entity identity plus a reference to the session that can materialize them. Touching an uninitialized proxy fires a SELECT through that session — transparently while it is open, impossibly after it closes. The exception text says exactly this: could not initialize proxy because there is no Session.

Detachment is the normal trigger. Entities leaving a @Transactional method detach from the closed session; the objects look complete in the debugger (ids and loaded fields show), but unloaded collections are hollow proxies. The first iteration, size check, or serialization touch explodes — often far from the service, in templates or mappers.

The debugger lies by loading. Inspecting a lazy collection in a debug session initializes it through the open debug-session context, making the bug vanish under observation. Reproduce without the debugger: log collection sizes inside versus outside the transaction and watch the outside call throw.

Scope is the takeaway: lazy is a contract to load later within the same session, not a promise the data follows the object. Code receiving entities must know which associations are loaded; code beyond the boundary must receive DTOs. Proxies never cross architectural boundaries — data does.

Order.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import jakarta.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity
public class Order {
    @Id
    @GeneratedValue
    private Long id;

    @OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
    private List<Line> lines = new ArrayList<>();

    public Long getId() {
        return id;
    }

    public List<Line> getLines() {
        return lines;
    }
}
📊 Production Insight
A debugger initializing proxies hid the bug through 6 reproduction attempts. Rule: reproduce lazy issues with logging, never the debugger — observation loads the evidence.
🎯 Key Takeaway
Lazy fields are session-bound proxies, not data. Detached entities carry hollow collections — map to DTOs before crossing boundaries.

JOIN FETCH: One Query, Fully Loaded, Per Use Case

JOIN FETCH loads the association in the same SELECT as the parent: one query, fully initialized collections, no follow-up SELECTs. The repository method above states its fetch plan plainly — orders with lines — and callers receive entities safe to traverse for the rest of the session. Distinct avoids duplicate parents from the join multiplication.

Scope each fetch to its use case. The report needs lines; the dropdown needs ids only; the detail page needs lines plus products. Three queries, three fetch plans, each loading exactly its screen. Shared findAll methods with fixed fetching either under-load (exceptions) or over-load (slowness) — per-use-case queries are the resolution.

Watch the two join-fetch pitfalls. Multiple collection fetch-joins in one query produce cartesian products (use one plus batch fetching for the rest), and fetch-joins with pagination in memory (Hibernate warns when the join breaks row-to-entity mapping). For paged collections, fetch the page of parents first, then batch-load their collections in a second query.

JOIN FETCH is the sharpest tool for single-purpose reads: explicit, local, and visible in the repository where reviewers can count queries. When three screens need three shapes, three methods beat one clever default.

OrderRepo.javaJAVA
1
2
3
4
5
6
7
8
9
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.ListCrudRepository;
import java.util.List;

public interface OrderRepo extends ListCrudRepository<Order, Long> {
    @Query("select distinct o from Order o left join fetch o.lines where o.id in :ids")
    List<Order> findWithLines(List<Long> ids);
}
📊 Production Insight
A shared findAll under-loaded every screen until per-use-case fetch methods cut one page from 800 queries to 3. Rule: name repository methods by screen shape — findWithLines, not findAll.
🎯 Key Takeaway
Fetch-join per use case: one query per screen shape, distinct against duplication, paginate parents separately from collections.

EntityGraphs: Reusable Fetch Plans Without JPQL Surgery

EntityGraphs declare reusable fetch plans: named graphs on the entity plus loadgraph or fetchgraph hints at query time. Where JOIN FETCH hardcodes the plan into JPQL, graphs separate what to load from how to query — the same base query renders with different shapes per caller.

The two hint types differ in strictness. Fetchgraph loads only the named attributes (everything else stays lazy); loadgraph loads the named attributes plus defaults. Reports wanting exact columns choose fetchgraph; detail pages wanting the usual graph plus extras choose loadgraph. The distinction prevents both under-loading and over-loading from one mechanism.

Graphs compose with Spring Data through @EntityGraph annotations on repository methods, keeping the plan beside the query it serves. Reviewers see the fetch shape without parsing JPQL joins, and plan changes stay one annotation wide instead of rewriting queries.

Prefer graphs when several queries share shapes or when JPQL grows unreadable. Prefer JOIN FETCH for one-off plans where the query and its shape belong together. Both beat the alternatives — EAGER defaults that tax every query, and OSIV that taxes every page. Record per-query counts in code comments where fetch plans are non-obvious.

OrderGraphs.javaJAVA
1
2
3
4
5
6
7
8
9
10
import jakarta.persistence.EntityGraph;
import jakarta.persistence.NamedAttributeNode;
import jakarta.persistence.NamedEntityGraph;
import jakarta.persistence.NamedEntityGraph;

@NamedEntityGraph(name = "order.withLines",
        attributeNodes = @NamedAttributeNode("lines"))
class OrderGraphs {
}
📊 Production Insight
Five JPQL variants of one query collapsed into one query plus three graphs — reviewable plans, countable queries. Rule: when fetch plans outnumber queries, move the plans into graphs.
🎯 Key Takeaway
Named graphs plus fetch/loadgraph hints reuse fetch shapes across queries. Fetchgraph for exact, loadgraph for plus-defaults.

DTO Projections: Data Without Proxies

DTO projections skip entities entirely: the query selects exactly the columns the screen needs into an interface or record. No proxies, no sessions, no lazy anything — the result is plain data safe to serialize, cache, and cross boundaries. The projection above serves a list page with three columns and zero association machinery.

Projections are the fastest option by construction. Selecting 6 columns over 10,000 rows beats materializing 10,000 entities plus 40,000 line items by orders of magnitude — the month-end report fell from 41 seconds to under a second on this change alone. Less data moved, less memory held, fewer queries run.

They also document the contract. The interface lists precisely what the screen consumes; adding a column means editing the projection and the query together. Consumers cannot drift into lazy traversal because there is nothing lazy to traverse — the architecture makes the exception unrepresentable.

Default to projections for reads. Reserve full entities for writes and complex domain logic that genuinely needs managed state. Most screens display data; displaying data needs DTOs, not persistence context passengers. Share graph names across teams through the domain module to avoid duplicates.

OrderSummary.javaJAVA
1
2
3
4
5
6
7
8
public interface OrderSummary {
    Long getId();

    String getCustomer();

    long getLineCount();
}
📊 Production Insight
A 6-column projection replaced entity serialization and cut month-end from 41 seconds to 900 ms. Rule: read screens project; only writes load entities.
🎯 Key Takeaway
Projections select exact columns into plain data — no proxies, no sessions, fastest reads. Default to DTOs for every read screen.

Open-Session-in-View: the Painkiller That Spreads the Infection

Open-Session-in-View holds the Hibernate session open through view rendering so lazy touches succeed. The exception disappears — and every template loop, serializer walk, and nested include fires its own SELECT on a connection checked out for the whole render. What threw once now queries 400 times.

The costs compound under load. Connections held through rendering starve the pool: 3 concurrent reports held 48 connections and queued the entire API. Each page's query count scales with data volume, so growth converts linear pages into quadratic outages. Nothing errors; everything slows.

Disabling OSIV converts the slowness back into the exception — deliberately. Each LazyInitializationException then names one unloaded association, which earns one fetch plan: join, graph, or projection. The migration is mechanical: disable, run the suite, fix each failure with a declared plan, repeat until green.

Spring Boot's default has moved against OSIV, and new projects should keep it off from scaffolding. Audit inherited projects for the flag the way you would audit any performance-critical default: explicitly, with query counts before and after. Convenience that scales with your data is not convenience.

⚠ OSIV Hides the Disease
OSIV trades a loud exception for silent N+1: hundreds of queries on connections held through rendering. Disable it and fix each fetch plan — the exception was pointing at real gaps.
📊 Production Insight
OSIV masked 40,000 queries behind zero errors until month-end saturated the pool. Rule: spring.jpa.open-in-view=false from scaffolding — convenience must never scale with data.
🎯 Key Takeaway
OSIV masks missing fetch plans with N+1 on held connections. Disable it; fix each revealed gap with a declared plan.

Boundaries and Proof: Transactions That Own the Read

Transactional boundaries decide where sessions live: one public service method owns the whole read — load, traverse, map — and only DTOs leave. Controllers receive data, templates render data, serializers emit data. No proxy survives the boundary because none crosses it.

Keep the annotation on public service methods. Private-method @Transactional is silently ignored by proxies, so the session never opens and lazy touches throw exactly as without it. Read-only hints on pure reads let the provider skip dirty checking — a free performance gain for reports.

Size boundaries to units of work, not to methods. One screen's read is one transaction even when it calls private helpers; splitting it across transactional methods detaches entities mid-thought. Helpers participate in the caller's transaction (the default propagation) rather than owning fragments.

Enforce with query-count tests: assert each endpoint runs its expected constant number of queries against fixture data. Counts catch N+1 that correctness tests miss and lazy gaps that OSIV would mask. A page whose queries scale with rows fails the test before it fails month-end. Keep OSIV disabled in every profile, including local dev, to prevent surprises.

ReportService.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;

@Service
public class ReportService {
    private final OrderRepo orders;

    public ReportService(OrderRepo orders) {
        this.orders = orders;
    }

    @Transactional(readOnly = true)
    public List<OrderSummary> monthly() {
        return orders.findAllProjectedBy();
    }
}
📊 Production Insight
Query-count assertions caught three N+1 regressions in a year, each pre-release. Rule: every read endpoint owns a count test — queries-per-page is a contract, not an observation.
🎯 Key Takeaway
One public @Transactional owns load-through-map; DTOs cross the boundary. Assert per-endpoint query counts in tests.
● Production incidentPOST-MORTEMseverity: high

40,000 Queries Hid Behind OSIV Until Month-End Hit 10,000 Rows

Symptom
Month-end reporting took 41 seconds and timed out for 60% of users; during 3 overlapping runs the whole application's API latency spiked 8x as the connection pool saturated. No errors appeared in logs — OSIV kept every lazy touch succeeding, one query at a time, 40,000 times.
Assumption
The report page was considered read-only and therefore safe — nobody load-tested it with production-like data volumes. OSIV was enabled since project scaffolding and regarded as framework default rather than a decision. Code review approved entity returns because every existing endpoint did the same.
Root cause
The report endpoint returned order entities with lazy line-item collections, serialized directly by Jackson under Open-Session-in-View. At 200 rows the page fired roughly 800 queries and took 3 seconds — unnoticed. At 10,000 rows it fired over 40,000 SELECTs, held 48 pooled connections through rendering, and took 41 seconds; 3 concurrent runs exhausted the pool and queued every other request. Disabling OSIV during diagnosis converted the slowness into LazyInitializationException, which finally named the unloaded collections.
Fix
OSIV was disabled and the report rebuilt on a DTO projection selecting exactly 6 columns; render time fell from 41 seconds to 900 milliseconds with 2 queries. Query-count assertions were added per endpoint in CI, entity returns were banned by review rule, and the connection pool stopped saturating — peak checkout dropped from 48 to 9 connections.
Key lesson
  • Load-test read endpoints with production data volumes — N+1 is invisible at 10 rows and fatal at 10,000.
  • Framework defaults are decisions: audit scaffolding flags like OSIV instead of inheriting them forever.
  • Cap query counts per endpoint in CI; count assertions catch performance regressions that correctness tests cannot.
Production debug guideFive checks that place every touch back inside a session.5 entries
Symptom · 01
could not initialize proxy on page render or API call
Fix
Read the trace for could not initialize proxy plus the entity and collection names, then find the touching line: template loop, Jackson serializer, or second service method. Move that touch inside the owning transaction or map it to a DTO first.
Symptom · 02
Suspecting N+1 behind the lazy touches
Fix
Enable spring.jpa.properties.hibernate.show_sql=true (or a query-count test) and load the page. One query per parent row plus one per collection item confirms N+1 — add JOIN FETCH for the rendered associations and recount until the page runs in constant queries.
Symptom · 03
No exception but pages run hundreds of queries
Fix
Check spring.jpa.open-in-view (and spring.jpa.properties.hibernate.enable_lazy_load_no_trans) in application.yml. If OSIV is true, the exception is masked and connections stay out through rendering — set it false and fix each revealed fetch plan deliberately.
Symptom · 04
Exception across two service methods or from private @Transactional
Fix
Move @Transactional to the public service method spanning load-through-map, and verify private-method annotations are gone (proxies ignore them). Retest: entities stay attached until mapping completes, and only DTOs cross to the controller.
Symptom · 05
Jackson triggering lazy loads during JSON rendering
Fix
Search Jackson frames in the trace for the serializing property, then replace entity serialization with a repository projection interface or record DTO mapped inside the transaction. Re-hit the endpoint and confirm flat query counts plus no proxy frames.
LazyInitializationException Causes Compared
Root CauseHow to ConfirmFixPrevention
Lazy touch after session closedCould not initialize proxy message outside @TransactionalFetch inside the transaction; return DTOsServices return DTOs, never live entities
Template iterating lazy collectionsTrace points at view rendering touching lines/itemsMap to DTO in service; template renders dataNo entity traversal in views or serializers
OSIV disabled exposing the gapException appears right after removing OSIVFix fetching per query — never re-enable OSIV blindlyOSIV off by default; per-query fetch plans
Split transactions detaching earlyEntities loaded in method A, touched in method BOne @Transactional owning load-through-mapService methods own whole units of work
JSON serializing lazy proxiesJackson frames in trace; N+1 or throw on nested fieldsProject to DTOs/interfaces inside the sessionControllers serialize DTOs only
⚙ Quick Reference
5 commands from this guide
FileCommand / CodePurpose
Order.java@EntityProxies and Detachment
OrderRepo.javapublic interface OrderRepo extends ListCrudRepository {JOIN FETCH
OrderGraphs.java@NamedEntityGraph(name = "order.withLines",EntityGraphs
OrderSummary.javapublic interface OrderSummary {DTO Projections
ReportService.java@ServiceBoundaries and Proof

Key takeaways

1
Lazy proxies need an open session
touching them after close throws could not initialize proxy.
2
Decide the response shape inside the transaction
fetch there, map to DTOs, return data.
3
OSIV hides the exception while multiplying N+1 queries and holding connections
leave it off.
4
Use JOIN FETCH per query, EntityGraphs for reusable plans, DTO projections for exact columns.
5
One @Transactional public method owns load-through-map; never split a read across transactions.
6
Assert query counts per endpoint in tests
counts catch N+1 that exceptions cannot.

Common mistakes to avoid

5 patterns
×

Returning entities to the view and lazy-loading in templates

Symptom
Templates iterate order.lines and throw per row. The transaction closed at the service exit, but the template's first lazy touch happens milliseconds later in the view layer.
Fix
Initialize what the view needs inside the transaction (touch the collection, or fetch-join it), or map to a DTO before returning. The service boundary decides the shape; the view renders decided data.
×

Enabling Open-Session-in-View to silence the exception

Symptom
Exception vanishes, N+1 queries explode, and connections stay checked out through view rendering. The page that threw once now runs 400 queries per render.
Fix
Delete the annotation and fix the fetching: join-fetch, entity graph, or DTO projection per query. Each screen declares what it needs; the framework stops papering over the gaps.
×

Marking everything FetchType.EAGER

Symptom
Lazy exceptions stop, but every query on the entity drags its whole graph. List pages slow 5x and cartesian products appear in join-heavy entities.
Fix
Fetch the needed associations in the same query with JOIN FETCH or an entity graph, selected per use case. Eager-by-default fixes one screen and taxes every other query on the entity.
×

Splitting one read across several @Transactional methods

Symptom
First method's entities detach at its exit; the second method touches their lazy collections and throws. Transaction-per-method chops a single unit of work into detached fragments.
Fix
Keep @Transactional at the service method that owns the whole read: load, traverse, and map inside it. Controllers and templates receive detached DTOs, never live entities.
×

Serializing entities directly to JSON

Symptom
Jackson traverses lazy proxies during rendering, throwing outside the session or firing N+1 SELECTs per nested collection. API latency jumps and payloads include half the database.
Fix
Map entities to DTOs (records or projection interfaces) inside the transaction and serialize those. Jackson then touches plain data with no proxy behind it — no session, no exception, no surprise queries.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What throws LazyInitializationException?
Q02SENIOR
Why can't the proxy just load the data later?
Q03SENIOR
What does OSIV cost and what replaces it?
Q04SENIOR
Why do split transactions throw on linked entities?
Q05SENIOR
Design a service layer where this exception is impossible.
Q01 of 05JUNIOR

What throws LazyInitializationException?

ANSWER
It is thrown when code touches an uninitialized lazy association after the Hibernate session closed — typically a detached entity in a view, serializer, or second method. You confirm with the could not initialize proxy message and fix by loading inside the session or mapping to a DTO.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
What exactly does could not initialize proxy mean?
02
What does Open-Session-in-View actually do?
03
Why not just make everything EAGER?
04
Can @Transactional on private methods cause this?
05
JOIN FETCH versus EntityGraph versus DTO projection?
06
How do I stop Jackson from triggering lazy loads?
N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.

Follow
Verified
production tested
September 23, 2026
last updated
1,905
articles · all by Naren
🔥

That's Spring. Mark it forged?

5 min read · try the examples if you haven't

Previous
Spring BeanCreationException Fix
3 / 3 · Spring