Transactions in JPA & Spring
Transactions in JPA & Spring
A transaction is a unit of work that either completes in full or leaves the database completely unchanged. Without transactions, a crash halfway through a multi-step operation — say, debiting one bank account before crediting another — produces corrupt, inconsistent data. Transactions are the safety net that prevents this.
Spring and JPA together make transaction management almost invisible, hiding the ugly EntityTransaction.begin()/commit()/rollback() boilerplate behind a single annotation: @Transactional. This lesson explains what that annotation actually does, what guarantees it provides, and the patterns every production developer must follow.
The ACID Guarantees
Every relational database transaction is expected to satisfy four properties, collectively called ACID:
- Atomicity — all operations in the transaction succeed together, or none of them do. If any step throws an exception, the entire transaction is rolled back.
- Consistency — a transaction takes the database from one valid state to another. Constraints, foreign keys, and application-level invariants must hold both before and after.
- Isolation — concurrent transactions do not see each other's intermediate, uncommitted changes. The degree of isolation is configurable (covered in Lesson 3).
- Durability — once a transaction is committed, its changes survive crashes, power failures, and restarts. The database writes to durable storage (WAL logs, etc.) before acknowledging the commit.
@Transactional provides Atomicity and (via the database) Consistency and Durability automatically. Isolation is a database-level knob you can tune — more on that in Lesson 3.
How Spring Implements @Transactional
Spring wraps your bean in a JDK dynamic proxy (or a CGLIB subclass proxy for classes). When a caller invokes a @Transactional method, the proxy intercepts the call, opens a transaction on the EntityManager/DataSource, delegates to your method, and then either commits or rolls back. Your business code never touches the transaction object directly.
Notice: if the InsufficientStockException is thrown, neither the inventory update nor the order insert reaches the database. That is atomicity in action.
Default Rollback Rules
Spring rolls back a transaction when an unchecked exception (subclass of RuntimeException or Error) is thrown. It commits when a checked exception propagates — a legacy design decision that surprises many developers.
throws IOException and an IOException escapes the method, Spring will commit the partial work. Use @Transactional(rollbackFor = IOException.class) to opt in to rollback for checked exceptions — or, better, wrap checked exceptions in RuntimeException subtypes at the domain boundary.
You can fine-tune rollback behaviour with the annotation attributes:
The EntityManager and the Persistence Context
When you annotate a Spring Data repository method (or your own @Repository) with @Transactional, Spring binds an EntityManager to the current transaction. The EntityManager maintains a persistence context — an identity map of every entity it has loaded. Within one transaction, loading the same row twice returns the same Java object reference; Hibernate will not issue a second SELECT.
At commit time, Hibernate's dirty-checking mechanism compares every managed entity's current state to its snapshot taken at load time. Any field that changed triggers an automatic UPDATE — you do not need to call save() explicitly for entities you retrieved within the same transaction.
Self-Invocation — The Most Common Pitfall
Because @Transactional is implemented via a proxy, calling a @Transactional method from within the same class bypasses the proxy entirely and therefore has no transactional effect. This is the single most common Spring transaction bug.
The fix: extract buildSummary() into a separate Spring-managed bean, and inject that bean. Then calls go through the proxy and the transaction semantics apply correctly.
Placing @Transactional — Service vs Repository
Annotate at the service layer, not (only) the repository layer. Repository methods like save() and findById() already carry their own @Transactional from Spring Data, but a service method that calls two repositories needs an outer transaction so both operations participate in the same unit of work. If you omit the service-level annotation, each repository call commits independently — and you lose atomicity across them.
Summary
ACID guarantees are the contract your database upholds for every transaction. Spring's @Transactional proxy provides atomicity transparently: commit on clean return, roll back on unchecked exceptions (and on checked ones only when you explicitly opt in). The persistence context's dirty-checking saves you explicit save() calls, but self-invocation silently breaks the proxy — always inject a separate bean when you need transactional composition. Keep transactions as short as possible to minimise lock contention. The next lesson goes deeper into propagation: what happens when a transactional method calls another transactional method.