intermediate~2h

Entity Lifecycle, Caching & Transaction Boundaries

Closing out Part 5: the full entity state machine, the two-level cache, and exactly where a transaction boundary sits relative to all of it.

Every JPA entity is in exactly one of four states at any moment, and most confusing Hibernate bugs ("why didn't my change save," "why did this insert happen twice") come down to not knowing which state an object is actually in — directly extending Chapter 12 §2's state table into the full picture.

StateMeaningHow you get there
TransientA plain Java object — JPA has never heard of it; no row exists.new Author()
ManagedTracked by the persistence context (Chapter 12) — field changes are dirty-checked and flushed automatically, no explicit save needed.entityManager.persist(entity), or loading it via findById() / a query
DetachedWas managed once, but its persistence context has since closed — the object still holds data, but changes to it are no longer tracked or saved.The transaction/EntityManager ends while you're still holding a reference
RemovedMarked for deletion; still tracked until commit, when the row is actually deleted.entityManager.remove(entity)
Author author = new Author("Rowling"); // Transient — no row, not tracked authorRepository.save(author); // Managed — tracked; dirty checking is now live // ... transaction ends here ... author.setName("J.K. Rowling"); // Detached — this change is silently NEVER saved, no error

That silent-loss line — editing a Detached entity and having nothing happen — is exactly why Chapter 12's "dirty checking only works inside an open persistence context" rule matters in practice, not just in theory.

💻 Code example

Author author = new Author("Rowling"); // Transient — no row, not tracked authorRepository.save(author); // Managed — tracked; dirty checking is now live // ... transaction ends here ... author.setName("J.K. Rowling"); // Detached — this change is silently NEVER saved, no error
First-level cacheSecond-level cache
ScopePer EntityManager /transaction — this is the persistence context itself (Chapter 12)Shared across the entire application (or cluster, with a distributed cache provider)
Enabled by default?Yes — always on, cannot be disabledNo — must be explicitly configured and opted into per entity
LifetimeEnds when the transaction/EntityManager closesSurvives across transactions and requests, until explicit eviction or TTL expiry
@Entity @Cacheable @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) public class Country { @Id private String code; private String name; // rarely-changing reference data is the classic 2nd-level cache use case }

▲ Common mistake

Enabling second-level cache for frequently-updated entities (e.g. an Order that changes status constantly) creates real cache staleness and invalidation complexity for very little read-performance benefit — reserve it for genuinely slow-changing, read-heavy reference data (countries, currencies, product categories), not your core transactional entities.

💻 Code example

@Entity @Cacheable @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) public class Country { @Id private String code; private String name; // rarely-changing reference data is the classic 2nd-level cache use case }

◆ The problem

A lazily-loaded relationship (@OneToMany(fetch = FetchType.LAZY)) is really a proxy that fetches its real data on first access — but that fetch needs an open EntityManager /persistence context to actually run a query through. Access it after the transaction has ended, and you get LazyInitializationException, not silently stale or empty data.

@Transactional public Author getAuthor(Long id) { return authorRepository.findById(id).orElseThrow(); // transaction ends when this method returns } // in the controller, OUTSIDE any transaction: Author author = authorService.getAuthor(1L); author.getBooks().size(); // LazyInitializationException — no persistence context left to fetch through

This directly connects back to Chapter 09's propagation and Chapter 12's persistence-context lifetime: the transaction boundary is the persistence context's lifetime in the standard Spring+JPA setup, so "access lazy data only while the transaction is still open" and "access lazy data only while the persistence context is still open" are the same rule stated two ways.

FixHow
Fetch what you need inside the transactionReturn a DTO mapped while still inside the @Transactional method, rather than the raw entity.
JOIN FETCHExplicitly fetch the relationship eagerly for this specific query, in one round trip.
Open Session in View (discouraged)Keeps the persistence context open for the entire web request — convenient, but hides N+1 queries and blurs the transaction boundary; generally considered an anti-pattern for anything beyond small apps.

💻 Code example

@Transactional public Author getAuthor(Long id) { return authorRepository.findById(id).orElseThrow(); // transaction ends when this method returns } // in the controller, OUTSIDE any transaction: Author author = authorService.getAuthor(1L); author.getBooks().size(); // LazyInitializationException — no persistence context left to fetch through

Want a visual for this concept?

Generate a diagram tailored to “Entity Lifecycle, Caching & Transaction Boundaries” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.

Sign in to generate a visual →

Practice quiz

Next Step

Continue to MongoDB Transactions← Back to all Transaction Mastery chapters