← Back to the section

When you work with Hibernate, there is an invisible middleman between your Java code and the database — the persistence context. It decides when and what to send to the database, and it makes sure that the same table row does not turn into two different objects in memory.

What the persistence context is

The persistence context is a working area that Hibernate creates for the duration of a unit of work (usually a transaction). Every entity you touch inside that area is under Hibernate's supervision.

In JPA terms, the persistence context is created by the EntityManager. In Hibernate its counterpart is the Session. In practice Spring manages this for you: a single EntityManager lives for exactly one transaction.

@Service
@Transactional
public class OrderService {

    private final EntityManager em;

    public OrderService(EntityManager em) {
        this.em = em;
    }

    public void confirm(Long orderId) {
        Order order = em.find(Order.class, orderId); // the entity is now managed
        order.setStatus(OrderStatus.CONFIRMED);       // Hibernate sees the change
        // em.persist() is not needed — flush will write the UPDATE itself
    }
}

Identity map: one object per row

The first thing the persistence context does is keep an identity map. This is a "primary key → object" table that guarantees: if you load the same row twice within a single persistence context, you get the same Java object.

Order a = em.find(Order.class, 1L);
Order b = em.find(Order.class, 1L);

System.out.println(a == b); // true — the same object, the second SELECT was never executed

The second find does not hit the database at all — Hibernate looks in the identity map and returns the object it already has. This is Hibernate's first-level cache (L1 cache), built in and always enabled. For more on caching, see the article /hibernate/caching/.

Four entity states

At any moment every entity is in one of four states. Transitions between them happen through explicit EntityManager calls or through the completion of a transaction.

diagram

Transient

The object is created with new, but Hibernate knows nothing about it. There is no primary key and no link to the database.

Order order = new Order(); // transient
order.setCustomerId(42L);

Managed

The entity is in the persistence context — Hibernate is watching it. Any change to a field will be synchronized with the database automatically on flush.

An entity becomes managed after:

  • em.persist(entity) — for new objects,
  • em.find(...) or a JPQL query — for objects loaded from the database,
  • em.merge(detachedEntity) — to bring a detached object back.

Detached

The entity was managed, but the persistence context closed (the transaction finished) or you explicitly called em.detach(entity). The object still exists in memory and has a primary key, but Hibernate no longer tracks it.

Order loaded;

// first transaction
try (var tx = ...) {
    loaded = em.find(Order.class, 1L); // managed
} // transaction closed → loaded became detached

loaded.setStatus(OrderStatus.CANCELLED); // the change will NOT be sent to the database automatically

To bring a detached entity back under Hibernate's control, use em.merge(loaded) in a new transaction. merge creates a new managed object with the data of the one you passed in — it does not modify the passed-in object directly.

Removed

The entity is marked for deletion. On the next flush Hibernate will execute a DELETE.

Order order = em.find(Order.class, 1L); // managed
em.remove(order); // marked as removed
// on flush → DELETE FROM orders WHERE id = 1

Dirty checking: where an UPDATE without an explicit save comes from

One of Hibernate's often-surprising features is dirty checking. At the moment of flush, Hibernate compares the current state of every managed entity against the snapshot it took when the entity was loaded. If the fields have changed, an UPDATE is generated automatically.

In short: a managed entity is a live link to the database, not just an object in memory.

@Transactional
public void updateEmail(Long userId, String newEmail) {
    User user = em.find(User.class, userId); // Hibernate stores a snapshot
    user.setEmail(newEmail);                 // change a field
    // there is no em.save() and none is needed
    // on flush: UPDATE users SET email = ? WHERE id = ?
}

Dirty checking only works for managed entities. For transient and detached entities Hibernate performs no UPDATE.

Flush: when changes reach the database

Flush is the synchronization of the persistence context state with the database. Hibernate executes SQL, but the transaction is not yet committed — a rollback is still possible.

By default, a flush happens:

  • before the transaction commit,
  • before a JPQL/HQL query, if there are pending changes for the same tables,
  • on an explicit call to em.flush().
@Transactional
public void transfer(Long fromId, Long toId, int amount) {
    Account from = em.find(Account.class, fromId);
    Account to   = em.find(Account.class, toId);

    from.deduct(amount);
    to.add(amount);

    // flush + commit on leaving the transaction
    // → two UPDATEs in a single transaction
}

A manual em.flush() is rarely needed — usually only when, after changing an entity, you need to run a native SQL query against the same table right away.

LazyInitializationException — an attempt to access a lazy collection outside the persistence context (after the transaction closed). The entity became detached, and Hibernate cannot issue a proxy query to the database.

// WRONG: the transaction closed, items is a detached lazy collection
Order order = orderService.findById(1L);
order.getItems().size(); // LazyInitializationException

The fix is to load the data inside the transaction (JOIN FETCH, @EntityGraph) or use DTO projections. For more, see the article /hibernate/lazy-vs-eager/.

An accidental UPDATE caused by dirty checking — you load an entity for reading but accidentally change a field, and Hibernate writes an UPDATE. For read-only operations, use em.detach(entity) after loading, or annotate the method with @Transactional(readOnly = true) (Spring will set FlushMode.MANUAL).

Relationship to Spring Data JPA

If you work through a JpaRepository, everything described above works exactly the same way — the EntityManager is simply hidden inside Spring Data. The repository's save() method calls em.persist() for new entities and em.merge() for detached ones. Managed entities changed inside a transaction do not need to be saved explicitly by Spring Data. For more on how repositories are built, see /spring/data-jpa/.

In short

  • The persistence context is Hibernate's working area; it lives for the duration of a transaction.
  • The identity map guarantees: one table row = one Java object within a single persistence context.
  • Four entity states: transient (unknown to Hibernate), managed (under supervision), detached (known but not tracked), removed (marked for deletion).
  • Dirty checking: Hibernate generates an UPDATE itself when a managed entity has changed — no explicit save is needed.
  • Flush synchronizes changes with the database before a commit or before a query against the same table.
  • LazyInitializationException is a sign of accessing lazy data after the persistence context has closed.
  • Lazy and eager loading — how Hibernate decides when to go to the database for related data.
  • Transactions and locking — optimistic and pessimistic locks, @Version.
  • Common Hibernate pitfalls — N+1, accidental UPDATEs, cascade problems.
  • Spring Data JPA — repositories and how they work on top of the EntityManager.