ArchitectMind
  1. Home
  2. Tutorials
  3. Book 3 · Jakarta EE & Enterprise Java
  4. Volume IV
  5. Persistence Context
Book 3 · Volume IV — Persistence & Transaction Management · Topic 1.2

Persistence Context

EntityManager, Context, Managed entities

8 min readStage 1 · Foundations8 interview questions
On this page
  1. 1. · Concept & Definition
  2. 2. · Why It Matters
  3. 3. · How It Works
  4. 4. · Java / Jakarta EE Code Examples
  5. 5. · Configuration, Defaults & Diagnostics
  6. Deep Dive — Flush Modes and Statement Ordering
  7. Deep Dive — Entity States and the Context Boundary
  8. 6. · Production Usage Patterns
  9. 7. · Common Pitfalls & Misconceptions
  10. 8. · Best Practices
  11. 9. · Architect's Perspective & Trade-offs
  12. 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.

The persistence context between the EntityManager and the database
Figure 1.2.1 — The persistence context between the EntityManager and the database

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 LazyInitializationException means and where it happens.

3· How It Works

EntityManager

OperationEffect
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 / createNativeQueryCreates queries whose results become managed (unless projections)

Context

The three kinds of EntityManager and their context lifetimes
Figure 1.2.2 — The three kinds of EntityManager and their context lifetimes
Context typeStartsEndsTypical use
Transaction-scoped (container-managed)When a JTA transaction begins and the EM is usedAt commit/rollbackDefault in Jakarta EE services
Extended (container-managed)With a @Stateful beanWhen the bean is removedMulti-request conversations
Application-managedemf.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 entityConsequence
Tracked by the contextField changes are detected and written at flush
Unique per primary keyfind twice returns the same instance (==)
Associated with a transaction (JTA)Changes are committed or rolled back with it
Holds a loaded-state snapshotMemory cost per entity; dirty check compares against it
Can be flushed at any timeConstraint violations may surface later than the setter call

4· Java / Jakarta EE Code Examples

Transparent update: no explicit save call
@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
    }
}
Identity map, references, flush and clear
@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
}
Keeping the persistence context small in bulk work
// 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();
        }
    }
}
Calling persist outside a transaction
jakarta.persistence.TransactionRequiredException: No EntityManager with actual
        transaction
  available for current thread - cannot reliably process 'persist' call

5· Configuration, Defaults & Diagnostics

Setting / APIDefaultEffect
FlushModeTypeAUTOFlush before queries that may be affected; COMMIT defers to commit
@PersistenceContext(type)TRANSACTIONEXTENDED for stateful conversations
em.setProperty("jakarta.persistence.lock.timeout", …)providerPessimistic lock wait
em.isOpen() / contains(e)—Diagnostics: is an entity managed?
Hibernate statisticsoffgetEntityLoadCount, getFlushCount, getQueryExecutionCount

Deep Dive — Flush Modes and Statement Ordering

SituationAUTO (default)COMMIT
Before a JPQL query touching changed entitiesFlush happensNo flush — query may not see changes
Before a native SQL queryProvider may not detect the overlapNo flush
At transaction commitFlush happensFlush happens
Explicit em.flush()Flush happensFlush 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.

Forcing statement order with an explicit flush
@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
}
Ordered and batched statements at flush, with statistics
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 4

Deep 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.

Loading inside the boundary, and merging back
// 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 strategyMemoryStaleness riskComplexityUse when
Transaction-scopedLowLowLowDefault for services
ExtendedHigherHigherMediumMulti-step editing of one aggregate
Application-managedControlledControlledMediumBatch, tests, Java SE
Stateless (provider-specific) / JDBCMinimalNoneHigherVery large bulk loads

10· Interview Q&A

Q1BasicConcept

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.

Q2BasicTricky

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.

Q3IntermediateConcept

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.

Q4IntermediateConcept

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.

Q5Senior EngineerInternals

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.

Q6Senior EngineerInternals

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.

Q7Technical ArchitectInternals

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.

Q8Solution ArchitectDesign

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.

Newsletter

Get new tutorials every week

New ArchitectMind tutorials and interview questions, once a week. No spam, unsubscribe anytime.

Almost done! Check your inbox and click the confirmation link to complete your subscription.