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

your code persistence context database new Order()no keycontext is empty em.find(1)twice order.setStatus(PAID) commit map: key → object1 → Order@3f1snapshotstatus=NEW Order@3f1status=NEW Order@3f1status=PAID≠ SELECT … WHERE id = 1second em.find(1):from the map, no SQL flush: compared to snapshotUPDATE ordersSET status = ? 1. transient: created with new, the context knows nothing about it 2. managed: the object goes into the map, with a snapshot beside it 3. a setter changed a field — the snapshot stayed the same 4. flush: fields differ from the snapshot → UPDATE; after commit — detached

The context holds two things: a "key → object" map, so that one table row never becomes two objects, and a snapshot of the fields taken at load time. A second em.find with the same key answers from the map and never touches the database. On flush Hibernate compares every managed entity with its snapshot and writes an UPDATE for the fields that differ; once the transaction is committed the context closes and the object becomes detached.

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: usually a single EntityManager lives for exactly one transaction. The word "usually" matters here — in a Spring Boot web application the Open Session In View mode is enabled by default, and then the context lives for the whole HTTP request, outliving the transaction boundaries inside it.

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

Transient (new)

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.

// inside the transaction
Order loaded = em.find(Order.class, 1L); // managed
// the transaction finished → 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.

The same mechanics fit into plain Java, with no database and no Hibernate: a "key → object" map, a snapshot of the fields taken at load time, and a comparison against that snapshot on flush.

live example

import java.util.HashMap;
import java.util.Map;

public class PersistenceContextDemo {

    static class Order {
        final long id;
        String status;
        Order(long id, String status) { this.id = id; this.status = status; }
    }

    static class Context {
        private final Map<Long, Order> identityMap = new HashMap<>();
        private final Map<Long, String> snapshot = new HashMap<>();
        int selects;

        Order find(long id) {
            Order known = identityMap.get(id);
            if (known != null) {
                return known;
            }
            selects++;
            Order loaded = new Order(id, "NEW");
            identityMap.put(id, loaded);
            snapshot.put(id, loaded.status);
            return loaded;
        }

        void flush() {
            for (Order order : identityMap.values()) {
                if (!order.status.equals(snapshot.get(order.id))) {
                    System.out.println("UPDATE orders SET status = '" + order.status
                            + "' WHERE id = " + order.id);
                    snapshot.put(order.id, order.status);
                }
            }
        }
    }

    public static void main(String[] args) {
        Context em = new Context();

        Order first = em.find(1L);
        Order second = em.find(1L);
        System.out.println("first == second: " + (first == second));
        System.out.println("queries sent: " + em.selects);

        first.status = "CONFIRMED";
        System.out.println("flush after the change:");
        em.flush();

        System.out.println("flush with nothing changed:");
        em.flush();
        System.out.println("(silence — the snapshot matches)");
    }
}
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →

Both find calls returned the same object, only one query went to the "database", and the UPDATE appeared only after the field changed. The second flush stays silent: the snapshot has already been refreshed, so there is nothing to compare.

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().

A manual em.flush() is rarely needed. In Spring Boot, Hibernate flushes pending changes before a native query on its own, so the usual case of "I changed an entity and now read it back with SQL" is covered without your help. The real reasons are to get a database-generated identifier before the transaction ends, or to catch a constraint violation right here rather than at commit time.

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

The order of SQL on flush is not the order of your code

Another consequence of the buffering nature of the persistence context: the accumulated changes reach the database not in the order your code is written, but grouped by operation type — all INSERTs first, then UPDATEs, then DELETEs. The classic trap: the code deletes a row with a unique value and inserts a new one with the same value — and on flush the INSERT goes to the database before the DELETE, so the transaction fails on a uniqueness violation even though the code "reads correctly". The cure is an explicit entityManager.flush() between the delete and the insert, or reworking the pair of operations into a single UPDATE.

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, and the SQL leaves in groups: INSERTs, then UPDATEs, then DELETEs.
  • LazyInitializationException is a sign of accessing lazy data after the persistence context has closed.