skip to content

How does JpaTransactionManager make a Spring Data repository and a @PersistenceContext EntityManager share the same persistence context within one transaction?

level: middleimportance: should knowfreq 45%

answer

  1. EntityManagerHolder bound in TransactionSynchronizationManager
  2. key = EntityManagerFactory, value = holder
  3. @PersistenceContext -> shared EM proxy -> doGetTransactionalEntityManager
  4. one first-level cache + one flush queue + one connection
  5. no tx = new EM per call = detached

basics

~20 s

On transaction start it creates one EntityManager, wraps it in an EntityManagerHolder, and binds it to the thread via TransactionSynchronizationManager keyed by the EntityManagerFactory. All JPA code in that transaction looks up and reuses that same bound EntityManager.

solid answer

~40 s

JpaTransactionManager opens a single EntityManager for the transaction and registers it as an EntityManagerHolder in Spring's TransactionSynchronizationManager, keyed by the EntityManagerFactory. Repositories and beans don't get a raw EntityManager — the one injected via @PersistenceContext is a shared proxy, and Spring Data goes through EntityManagerFactoryUtils.doGetTransactionalEntityManager. Both resolve to the holder bound to the current thread, so every operation runs against one persistence context: one first-level cache, one flush queue, one JDBC connection. That's what makes dirty-checking and lazy loading work across service and repository calls, and why an entity loaded by one repository is `==` identical when loaded again in the same transaction. Outside any transaction, @PersistenceContext falls back to creating a short-lived EntityManager per call (unless OSIV is active), so there's no shared context.

code

java · 18 lines
java
@Service
class CatalogService {

    @PersistenceContext
    private EntityManager em; // shared proxy, NOT a raw EntityManager

    private final ProductRepository products; // Spring Data JPA
    CatalogService(ProductRepository products) { this.products = products; }

    @Transactional
    public void demo(long id) {
        Product viaRepo = products.findById(id).orElseThrow();
        Product viaEm   = em.find(Product.class, id);
        // Same bound EntityManager -> same first-level cache -> identity guarantee:
        assert viaRepo == viaEm;
        viaRepo.setName("renamed"); // dirty; flushed at commit, no explicit save needed
    }
}

go deeper

for a junior

Know that everything in one @Transactional shares one EntityManager.

for a middle

Name EntityManagerHolder + TransactionSynchronizationManager and the shared-proxy resolution path.

for a senior

Explain identity/first-level-cache consequences and the no-transaction detached-entity pitfall.

for a principal

Reason about multi-EMF keying, OSIV trade-offs, and REQUIRES_NEW persistence-context isolation.

**The problem being solved.** A service method calls several repositories and maybe injects an `EntityManager` directly. For dirty checking, identity, and lazy loading to work, all of these must operate on the *same* persistence context and the *same* database transaction. Something has to make that sharing happen — that something is `JpaTransactionManager` plus Spring's thread-bound resource registry. **TransactionSynchronizationManager.** Spring stores active transactional resources in `TransactionSynchronizationManager`, a class backed by `ThreadLocal` maps. Resources are bound under a key. For JPA the key is the `EntityManagerFactory` and the value is an `EntityManagerHolder` (a small wrapper around one `EntityManager` plus flags like rollback-only and whether a transaction is active). **What JpaTransactionManager.doBegin does.** When a new transaction starts, `doBegin`: 1. Asks the `EntityManagerFactory` for a new `EntityManager` (`createEntityManager()`). 2. Begins a resource-local transaction (`em.getTransaction().begin()`), or joins JTA. 3. Wraps the manager in an `EntityManagerHolder` and calls `TransactionSynchronizationManager.bindResource(emf, holder)`. It also usually exposes the underlying JDBC `Connection` (see the JdbcTemplate-sharing question). **How consumers find it.** - `@PersistenceContext EntityManager em` does NOT inject a plain manager. Spring injects a **shared EntityManager proxy** (a `SharedEntityManagerBean`). Every call on that proxy delegates to `EntityManagerFactoryUtils.doGetTransactionalEntityManager(emf, ...)`, which looks up the bound holder for the current thread and uses its `EntityManager`. - Spring Data JPA repositories internally obtain their `EntityManager` the same way, so `save`, `findById`, JPQL, etc. all hit the bound instance. Because both paths converge on the one holder, the whole transaction shares: **one first-level cache** (so the same primary key returns the same object instance — identity guarantee), **one flush queue** (dirty changes accumulate and flush together), and **one JDBC connection**. **End of transaction.** On commit/rollback, `JpaTransactionManager` unbinds the resource (`unbindResource`), commits or rolls back the resource-local transaction, and closes the `EntityManager`. **Edge cases / gotchas.** - **No transaction, no sharing.** Call a repository outside `@Transactional` and each call gets its own throwaway `EntityManager` (a new persistence context per query), so entities returned are detached immediately and you can hit `LazyInitializationException`. - **Open Session/EntityManager In View (OSIV).** Spring Boot enables OSIV by default for web apps, which binds an `EntityManager` for the whole HTTP request even outside `@Transactional`, changing this behavior. It keeps lazy loading working in the view but holds resources longer — often disabled deliberately. - **New nested transaction (`REQUIRES_NEW`).** Suspends the current holder, binds a fresh one, so the inner block has its own persistence context and connection — changes there are isolated until its own commit. - **Wrong EntityManagerFactory key.** With multiple persistence units, a repository bound to EMF-A won't see a transaction started by a `JpaTransactionManager` configured for EMF-B; you get an unexpected new transaction or a 'no transaction in progress' situation.

  • What happens if you call a repository method with no active transaction?
    The shared EntityManager proxy creates a fresh EntityManager just for that call and closes it after, so returned entities are detached and lazy associations fail — unless OSIV is active for the request.
  • Why does @PersistenceContext inject a proxy instead of a real EntityManager?
    A real EntityManager is not thread-safe and is transaction-scoped. The proxy is a stable singleton-injectable object that, per call, resolves to the correct thread-bound transactional EntityManager (or a temporary one).

saying these in an interview costs you the question

  • Believing @PersistenceContext injects a raw, singleton EntityManager shared across threads
  • Thinking repositories open their own independent connection/persistence context per call inside a transaction
  • Not knowing entities are detached when accessed outside a transaction (LazyInitializationException)

context