Hibernate hands back a proxy object for a lazily mapped association instead of the real entity. What does that proxy hold internally, and why does calling a getter on it after the Hibernate Session that produced it has been closed fail?
answer
- Generated subclass + LazyInitializer
- Holds id + session reference
- First real getter = deferred SELECT
- Session closed/disconnected -> throw, not reconnect
- Collections: PersistentBag with initialized flag
basics
~20 sA lazy proxy is a generated subclass holding an initializer with the target's identifier and a reference to the Session that created it. The first real getter call triggers a deferred SELECT; if that Session is closed the proxy has no connection to run it, so Hibernate throws instead of silently opening a new one.
solid answer
~50 sFor a lazy `@ManyToOne`, Hibernate returns a runtime-generated subclass of the entity (Byte Buddy) whose methods are intercepted. Inside sits a `LazyInitializer` that knows the entity name, the identifier value already read from the foreign-key column, and a reference to the owning `Session`. Lazy collections are analogous: `PersistentBag`/`PersistentSet` wrappers with an `initialized` flag and a session reference. On the first intercepted call the initializer asks its session to run the deferred `SELECT ... WHERE id = ?`, then delegates to the loaded target. If the session reference is null, closed, or disconnected — because the transaction committed and the `EntityManager` closed, or the entity was detached/cleared/evicted — Hibernate throws `LazyInitializationException: could not initialize proxy ... no Session`. It refuses deliberately: opening a fresh connection would execute the read outside the original transaction, at a different point in time and possibly a different snapshot, quietly breaking the consistency of the unit of work.
code
java · 8 linesEntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Order order = em.find(Order.class, 42L);
Customer c = order.getCustomer(); // proxy, no SELECT yet
em.getTransaction().commit();
em.close();
c.getName(); // LazyInitializationException: no Sessiongo deeper
Know that a lazy association returns a stand-in object, that touching it after the transaction ends throws, and that the data must be fetched while the persistence context is still open.
Be able to describe the proxy as a generated subclass with an initializer holding the id and the session, name where initialization is triggered, and explain that detach/clear cause the same failure as closing.
Explain why Hibernate deliberately refuses to reconnect (cross-snapshot reads, hidden per-object queries), and recognise the failure from a serializer stack trace, tracing it back to the fetch plan at load time.
Frame it as a contract question: which object graph is guaranteed loaded when an object crosses a boundary. Push that guarantee into query design or projections instead of relying on runtime luck about who holds a session.
## What a lazy proxy actually is When an association is mapped `FetchType.LAZY`, Hibernate does not load the associated row while loading the owner. It has the foreign-key value in hand, so it can hand you an object that *looks* like the target entity without querying for it. That object is a **proxy**: a runtime-generated subclass of your entity class (Byte Buddy since Hibernate 5.3, Javassist/CGLIB before that) in which every method is intercepted. Inside the proxy sits a `LazyInitializer`. It holds three things that matter: 1. the **entity name** (which mapped type it stands for), 2. the **identifier** already read from the FK column of the owner row, 3. a reference to the **session** (`SharedSessionContractImplementor`) that created it, plus a `target` field that is null until initialization. Lazy **collections** use a different but parallel mechanism: Hibernate replaces your `List`/`Set` field with a `PersistentBag`, `PersistentList` or `PersistentSet` wrapper carrying an `initialized` flag, the owning entity's key, and the same session reference. ## What "initialization" means The first time application code calls an intercepted method — `getName()`, `size()`, `iterator()`, `toString()` — the initializer runs the deferred query: `select ... from target where id = ?` for a proxy, or the collection load for a wrapper. From then on the proxy delegates every call to the loaded target and the collection behaves normally. Before running the query the initializer checks its session: is it non-null, open, and connected? If not, it throws `org.hibernate.LazyInitializationException` with a message such as `could not initialize proxy [com.acme.Order#42] - no Session`, or `failed to lazily initialize a collection of role: com.acme.Order.lines - could not initialize proxy - no Session`. ## Why closing the Session is fatal The session owns two things the load needs: the **JDBC connection** (in Hibernate's terms, the `JdbcCoordinator`) and the **first-level cache** into which the loaded entity would be registered. When the transaction commits and the `EntityManager`/`Session` closes, both are gone and every entity it managed becomes **detached**. Hibernate could technically open a fresh connection and query anyway. It refuses on purpose. That second read would happen after your transaction ended, on a different connection, against a different database snapshot — so a single object graph would mix data from two points in time. It would also hide the fact that you are issuing an unbounded, unbatched query per object touched. Failing loudly turns a silent correctness-and-performance problem into a compile-and-test-time bug. ## Detached is not the same as closed The session can still be open and you can still get the exception: `entityManager.detach(e)`, `clear()`, `evict()`, or serializing the entity and deserializing it elsewhere all sever the session reference. Similarly, `merge(detached)` returns a **new** managed instance — the old detached object still carries its dead proxies, so keeping a reference to the argument you passed in is a common trap. ## Where the failure surfaces Almost never at the mapping. It surfaces in a JSON serializer walking getters after the controller method returned, a template engine rendering a page, a mapper copying to a DTO, or a `toString()` in a log line. The stack trace therefore points at Jackson or the mapper rather than at the persistence code, which is why the exception feels mysterious the first time. ## Useful nuances - Calling the **identifier getter** on a proxy does not necessarily initialize it: if the entity uses property access (`@Id` on the getter) Hibernate knows the identifier method and returns the stored id without a query. With field access it usually initializes. `Hibernate.getId(proxy)` / `session.getIdentifier(proxy)` are the reliable ways. - `proxy instanceof SomeSubclass` and `getClass()` are unreliable for polymorphic associations, because the proxy is typed to the declared type, not the concrete subclass. - `Hibernate.isInitialized(x)` tells you whether a proxy or collection is loaded; `Hibernate.initialize(x)` forces the load — but only works while a session is still open, so it is a tool for building the graph *before* leaving the transaction, not for rescuing a detached object. - With build-time **bytecode enhancement** (lazy `@OneToOne`, lazy basic attributes), the entity instance itself carries an interceptor instead of being a subclass, but the failure mode is identical. ## Practical implication The exception is not a bug to suppress; it is Hibernate reporting that your fetch plan does not match your read plan. The fix always belongs where the data is loaded — decide *inside* the transaction exactly which graph the caller needs — not where the object is consumed.
- Does reading the identifier of an uninitialized proxy trigger a query?Not always. If the entity maps its identifier with property access (`@Id` on the getter), Hibernate knows which method returns the id and the proxy answers from the value it already holds, with no SQL. With field access the getter is just another intercepted method and typically initializes the proxy. `Hibernate.getId(proxy)` or `session.getIdentifier(proxy)` is the portable way to read it without a load.
- Why doesn't Hibernate just open a new connection and load the data on demand?Because the load would run outside the original transaction, on a different connection and a different snapshot, so one object graph would mix data from two points in time. It would also silently emit one unbatched query per object touched. Hibernate prefers to fail loudly so the fetch plan gets fixed at the query, where it belongs.
- Why does the stack trace usually point at the JSON serializer rather than at persistence code?Because the exception is thrown at the moment a getter is called, and the first component that walks every getter of a returned entity is usually the serializer or a mapper running after the transaction has committed. The mapping decision that caused it was made much earlier, when the entity was loaded.
The proxy is a claim ticket at a coat check: it carries the ticket number (the id) and the name of the counter that issued it. Once that counter has closed for the night, the ticket is still in your pocket but nobody can hand you the coat.
saying these in an interview costs you the question
- Saying the exception means the data "was not saved" or the association is null — the data is fine, it simply was never fetched.
- Claiming Hibernate could not reload it because the object is immutable, rather than because the session and its connection are gone.
- Believing the exception can only happen after the EntityManager closes, forgetting detach/clear/evict on an open session.
- Assuming `merge()` re-attaches the instance you passed in — it returns a different managed copy while your original keeps its dead proxies.
- Treating a null-check or a try/catch around the getter as a fix.