On this page
- 1. · Concept & Definition
- 2. · Why It Matters
- 3. · How It Works
- 4. · Java / Jakarta EE Code Examples
- 5. · Configuration, Defaults & Diagnostics
- Deep Dive — Flush Modes and Statement Ordering
- Deep Dive — Entity States and the Context Boundary
- 6. · Production Usage Patterns
- 7. · Common Pitfalls & Misconceptions
- 8. · Best Practices
- 9. · Architect's Perspective & Trade-offs
- 10. · Interview Q&A
1· Concept & Definition
The persistence context is the set of entity instances that an EntityManager currently manages. It acts as a unit of work (collecting changes and writing them at flush time) and as a first-level cache and identity map (one instance per primary key). Understanding when a context starts, when it flushes and when it ends explains almost all timing-related persistence behaviour.

2· Why It Matters
- Whether an entity is managed decides if changes are saved automatically.
- Flush timing decides when SQL is issued — and therefore when constraint violations appear.
- Context size drives memory use and dirty-checking cost in loops and batch jobs.
- Context boundaries determine what
LazyInitializationExceptionmeans and where it happens.
3· How It Works
EntityManager
| Operation | Effect |
|---|---|
persist(e) | Makes a new entity managed; INSERT at flush |
find(Class, id) | Returns the managed instance, from the context if present, else from the database |
getReference(Class, id) | Returns a lazy proxy without hitting the database until accessed |
merge(e) | Copies state from a detached instance into a managed one and returns it |
remove(e) | Schedules DELETE at flush (entity must be managed) |
refresh(e) | Reloads state from the database, discarding in-memory changes |
detach(e) / clear() | Removes one entity / all entities from the context |
flush() | Writes pending changes now |
lock(e, mode) | Applies optimistic or pessimistic locking |
createQuery / createNativeQuery | Creates queries whose results become managed (unless projections) |
Context

| Context type | Starts | Ends | Typical use |
|---|---|---|---|
| Transaction-scoped (container-managed) | When a JTA transaction begins and the EM is used | At commit/rollback | Default in Jakarta EE services |
| Extended (container-managed) | With a @Stateful bean | When the bean is removed | Multi-request conversations |
| Application-managed | emf.createEntityManager() | em.close() | Java SE, tests, batch, custom control |
In a JTA environment, several components that share the same transaction share the same transaction-scoped persistence context — this is what makes a service method composed of several repository calls behave as one unit of work.
Managed entities
| Property of a managed entity | Consequence |
|---|---|
| Tracked by the context | Field changes are detected and written at flush |
| Unique per primary key | find twice returns the same instance (==) |
| Associated with a transaction (JTA) | Changes are committed or rolled back with it |
| Holds a loaded-state snapshot | Memory cost per entity; dirty check compares against it |
| Can be flushed at any time | Constraint violations may surface later than the setter call |
4· Java / Jakarta EE Code Examples
@ApplicationScoped
public class OrderService {
@PersistenceContext EntityManager em; // transaction-scoped
@Transactional
public void markPaid(long orderId) {
Order o = em.find(Order.class, orderId); // becomes managed
// no em.update() call exists
o.setStatus(OrderStatus.PAID);
o.setPaidAt(Instant.now());
// flush + commit → single UPDATE
}
}@Transactional
public void demonstrateContext(long id) {
Order a = em.find(Order.class, id);
// no SQL: served from the context
Order b = em.find(Order.class, id);
assert a == b; // identity map guarantee
Order ref = em.getReference(Order.class, id); // proxy, no SELECT yet
// write pending changes now
em.flush();
em.clear(); // detach everything
Order c = em.find(Order.class, id); // SELECT again; c != a
}// Batch loop: bound the context to keep memory and dirty-check cost flat
@Transactional
public void importAll(List<Row> rows) {
for (int i = 0; i < rows.size(); i++) {
em.persist(Customer.from(rows.get(i)));
// matches hibernate.jdbc.batch_size
if (i % 50 == 0) {
em.flush();
em.clear();
}
}
}jakarta.persistence.TransactionRequiredException: No EntityManager with actual
transaction
available for current thread - cannot reliably process 'persist' call5· Configuration, Defaults & Diagnostics
| Setting / API | Default | Effect |
|---|---|---|
FlushModeType | AUTO | Flush before queries that may be affected; COMMIT defers to commit |
@PersistenceContext(type) | TRANSACTION | EXTENDED for stateful conversations |
em.setProperty("jakarta.persistence.lock.timeout", …) | provider | Pessimistic lock wait |
em.isOpen() / contains(e) | — | Diagnostics: is an entity managed? |
| Hibernate statistics | off | getEntityLoadCount, getFlushCount, getQueryExecutionCount |
Deep Dive — Flush Modes and Statement Ordering
| Situation | AUTO (default) | COMMIT |
|---|---|---|
| Before a JPQL query touching changed entities | Flush happens | No flush — query may not see changes |
| Before a native SQL query | Provider may not detect the overlap | No flush |
| At transaction commit | Flush happens | Flush happens |
Explicit em.flush() | Flush happens | Flush happens |
At flush, the provider orders statements by operation type (inserts, updates, deletes) and by entity type, not by the order of the calls in your code. This matters for constraint-sensitive sequences: a delete-then-insert of the same unique key in one transaction may fail unless an explicit flush() separates them.
@Transactional
public void replaceTag(long productId, String oldTag, String newTag) {
Product p = em.find(Product.class, productId);
p.removeTag(oldTag);
em.flush(); // force the DELETE before the INSERT below
p.addTag(newTag); // same unique key (product_id, tag) reused
}Hibernate: /* insert */ insert into order_lines (...) values (?,?,?)
// batched together
Hibernate: /* insert */ insert into order_lines (...) values (?,?,?)
Hibernate: /* update */ update orders set status=?, version=? where id=? and version=?
Hibernate: /* delete */ delete from order_lines where id=?
Statistics: 3 JDBC statements, 2 batches, flush count 1, entities loaded 4Deep Dive — Entity States and the Context Boundary
Every entity instance is in one of four states relative to a persistence context. A new, or transient, instance has been created with new and is unknown to any context. A managed instance belongs to an open context and is tracked. A detached instance was managed by a context that has since ended or been cleared; it still holds its data, but changes to it are ignored. A removed instance is still managed but is scheduled for deletion at the next flush.
Detachment is what gives LazyInitializationException its meaning. A lazy association is a proxy or an uninitialised collection that can only load itself through the context that created it. Once the transaction commits and the context closes, touching that association, typically in a JSON serialiser or a view template, leaves Hibernate nothing to load from, so it throws. The fix is to load what the caller needs while the context is still open, with a fetch join, an entity graph or a DTO projection. Spring Boot's open-in-view default hides the exception by keeping the context open during rendering, but it lets the web layer issue unplanned queries, which is why Boot logs a warning about it.
// Load everything the response needs inside the transaction
@Transactional
public OrderDto load(long id) {
Order o = em.createQuery(
"select o from Order o join fetch o.lines where o.id = :id",
Order.class)
.setParameter("id", id)
.getSingleResult();
return OrderDto.from(o); // built while o is still managed
}
// merge copies a detached instance's state into a managed one
@Transactional
public Order update(Order detached) {
Order managed = em.merge(detached); // a different instance
// later changes must go to 'managed'; 'detached' stays untracked
return managed;
}6· Production Usage Patterns
- One transaction per business operation, with a transaction-scoped context shared by all repositories.
- Read endpoints running in read-only transactions, returning DTOs, so nothing is managed longer than needed.
- Explicit
flush()before native queries that must see pending changes. clear()at intervals in import and migration jobs.
7· Common Pitfalls & Misconceptions
- Expecting
em.persist()to issue SQL immediately — it usually happens at flush. - Modifying a detached entity and expecting it to be saved.
- Long transactions with thousands of managed entities — memory and slow dirty checking.
- Using an extended context as a cache across user interactions — stale data and locking issues.
- Opening an application-managed EntityManager without closing it — connection and memory leaks.
8· Best Practices
- Keep contexts short and aligned with transactions.
- Return DTOs from read operations instead of managed entities.
- Flush and clear periodically in bulk loops.
- Never share an EntityManager between threads: it is not thread-safe.
9· Architect's Perspective & Trade-offs
| Context strategy | Memory | Staleness risk | Complexity | Use when |
|---|---|---|---|---|
| Transaction-scoped | Low | Low | Low | Default for services |
| Extended | Higher | Higher | Medium | Multi-step editing of one aggregate |
| Application-managed | Controlled | Controlled | Medium | Batch, tests, Java SE |
| Stateless (provider-specific) / JDBC | Minimal | None | Higher | Very large bulk loads |
10· Interview Q&A
What is the persistence context?
The persistence context is the set of entity instances that one EntityManager currently manages. It plays three roles at once. It is an identity map, so within one context each database row is represented by exactly one Java object, and two finds for the same ID return the same instance. It is a first-level cache, so repeated finds do not hit the database. And it is a unit of work: it tracks changes to managed entities and writes them as inserts, updates and deletes at flush time, usually just before commit. In Jakarta EE the default context is transaction-scoped, beginning and ending with the JTA transaction.
Why is there no update() method in the EntityManager?
Because managed entities do not need one. When an entity is loaded or persisted, the persistence context keeps a snapshot of its state. At flush time the provider compares each managed entity with its snapshot, a process called dirty checking, and issues an UPDATE for anything that changed, so calling setStatus on a managed Order is enough. merge exists for a different situation: copying the state of a detached instance, for example one deserialised from a request, into a managed instance, which it returns. Developers used to other frameworks often call merge on entities that are already managed; it is harmless but redundant, and the returned object matters.
What is the difference between find() and getReference()?
find returns a fully initialised entity. It checks the persistence context first and, if the entity is not there, runs a SELECT immediately; if no row exists it returns null. getReference returns a reference, usually an uninitialised proxy, without querying the database, unless the entity is already in the context. The proxy loads its state only when a non-identifier attribute is accessed, and if the row is missing that access throws EntityNotFoundException. getReference is ideal when you only need the entity to set an association, such as order.setCustomer(em.getReference(Customer.class, id)), because it avoids a pointless SELECT. Accessing an uninitialised reference after the context closes fails.
When does a transaction-scoped persistence context end?
It ends when the JTA transaction it is bound to commits or rolls back. The container creates the context the first time a transaction-scoped EntityManager is used inside the transaction, shares it with every component using the same persistence unit in that transaction, flushes it before commit, and then closes it. At that point every entity it managed becomes detached: the objects still hold their data, but further changes are not tracked and uninitialised lazy associations can no longer load. On rollback the entities are detached too, and their in-memory state may no longer match the database, so they should be discarded rather than reused or merged back.
What does flush do, and when does it happen automatically?
Flush synchronises the persistence context with the database: the provider runs dirty checking and issues the pending INSERT, UPDATE and DELETE statements inside the current transaction. It does not commit, so the changes remain invisible to other transactions and can still be rolled back. With the default AUTO flush mode it happens automatically at commit and before any query whose results might be affected by pending changes, so a JPQL query sees your own uncommitted updates. Native queries are a gap, because the provider cannot always tell which tables they touch. Calling flush explicitly is useful to surface constraint violations early or to force statement order.
Why should the persistence context be cleared in long loops?
Because the cost of a persistence context grows with every entity it manages. Each managed entity is held twice, as the live object and as a snapshot of its loaded state, and nothing is released until the context ends. Worse, each flush dirty-checks every managed entity, so in a loop that persists 100,000 rows and flushes periodically, every flush gets slower and total work grows roughly quadratically. Calling flush and then clear every batch, typically matching hibernate.jdbc.batch_size, writes pending changes and detaches everything, keeping memory and flush time flat. Remember that entity references held from before the clear are now detached, so stop modifying them.
How do several repositories participate in one unit of work?
In a JTA environment, the persistence context is bound to the transaction, not to an EntityManager reference. When a @Transactional service method calls an order repository and an inventory repository, each injected with @PersistenceContext, both EntityManager proxies resolve to the same transaction-scoped context. An entity loaded by one repository is therefore the same instance the other sees, changes from both are flushed together, and they commit or roll back atomically. This propagation is why the transaction boundary belongs on the service method rather than on each repository. It works only within one persistence unit and one thread; a method running with REQUIRES_NEW gets a separate context.
What are the architectural implications of persistence-context boundaries?
Persistence-context boundaries are consistency boundaries. Everything changed within one context commits together, so the context's scope should match one business operation, ideally one aggregate per transaction, such as placing an order. Short, transaction-scoped contexts keep services stateless, so any instance can handle any request, which makes horizontal scaling and rolling deployments easy. Extended contexts keep managed entities across requests, creating server-side state that needs sticky sessions or replication and raises the risk of stale data. Boundaries also shape APIs: returning DTOs built inside the transaction avoids lazy-loading failures at the edge. In microservices no context spans services; cross-service consistency needs sagas or an outbox.