Appearance
@Transactional Part 1: ACID and How It Works
Chapter 12 showed AOP: a pointcut picks methods, an advice runs around them, and Spring applies it through a proxy. The most important use of AOP in Spring is transactions. This part covers:
- why transactions are needed: critical sections and ACID;
- what transaction code looks like by hand, and why that is a problem;
@Transactional: setup and use;- how it works inside: an AOP around advice.
(It also helps to know how databases handle concurrent access, which part 3, on isolation levels, explains.)
1. Critical sections
A critical section is a piece of code that reads and modifies a shared resource. A car-booking example:
| id | status |
|---|---|
| 1001 | Available |
text
read car 1001
if status is "Available":
update status to "Booked"Four customers try to book car 1001 at the same moment. Four requests run this code in parallel: all four read the row, all four see Available, and all four set it to Booked. Four customers are told they got the car. When several requests run a critical section in parallel without control, the data becomes inconsistent.
The solution is a transaction.
2. ACID
A transaction guarantees four properties, ACID. Take account A with ₹10, account B with ₹20, and a transfer of ₹5 from A to B. The transaction has two operations: debit A, credit B.
- Atomicity: the operations succeed all together or not at all. If the debit succeeds (A has ₹5) but the credit fails, the whole transaction is rolled back: the debit is undone and A has ₹10 again.
- Consistency: the database is consistent before and after the transaction. Before: 10 + 20. After a success: 5 + 25. After a failure: 10 + 20 again. Never 5 + 20, money that vanished.
- Isolation: transactions running in parallel do not interfere. Several transfers touching A at once each behave as if they ran alone; the database uses locks (or similar techniques) to put conflicting operations in order.
- Durability: once a transaction commits, its changes are stored permanently and survive a crash.
That is why financial applications rely on databases with ACID transactions.
3. By hand: begin, commit, rollback
Without help, every method that changes data looks like this:
text
BEGIN TRANSACTION
debit ₹5 from A ← the actual business logic
credit ₹5 to B ←
if all succeeded:
COMMIT
else:
ROLLBACK
END TRANSACTIONTwo lines are business logic; the rest is transaction handling. An application has hundreds of methods that write to the database (updateUser, updateBulkUsers, updateUserById, ...), and every one repeats the same begin/commit/rollback code: verbose and duplicated. That is exactly the kind of boilerplate AOP removes, and Spring does it with @Transactional.
4. Setup
@Transactional needs two dependencies:
A data starter that brings transaction support. For a relational database, Spring Data JPA:
xml<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency>(Some NoSQL databases support transactions too, through their own starters.)
A database driver. To follow along without installing a database, use H2, an in-memory database that runs inside the application:
xml<dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency>With H2 on the classpath, Spring Boot creates the database and connects to it with no further configuration. (The JPA module, chapters 23 to 32, covers connecting to real databases, entities and repositories in depth.)
@EnableTransactionManagement is what switches annotation-driven transactions on. Spring Boot's auto-configuration already adds it, so you normally do not write it:
java
@SpringBootApplication
@EnableTransactionManagement // optional: Spring Boot already does this
public class LearningspringbootApplication { ... }Without it (in a plain Spring application), @Transactional annotations would simply be ignored.
5. Using @Transactional
java
@Component
public class CarService {
@Transactional
public void updateCar() {
// read car 1001, check the status, update it
}
}The whole method now runs in one transaction: it is begun before the method starts; if the method finishes, the changes are committed; if it throws, they are rolled back.
- On a method: only that method is transactional.
- On the class: every method of the class is. Classic Spring applied it to
publicmethods only; with the class-based proxies Spring Boot uses (Spring 6),protectedand package-private methods are covered too.privatemethods never are: a proxy cannot override them (chapter 12).
6. How it works: AOP
@Transactional is AOP from chapter 12:
- a pointcut matches methods annotated
@Transactional(and methods of classes annotated with it); - for each match, an around advice runs: the method
invokeWithinTransactionofTransactionInterceptor(inherited from its parent classTransactionAspectSupport).
The advice, simplified from Spring's source:
java
// TransactionAspectSupport.invokeWithinTransaction (simplified)
TransactionInfo txInfo = createTransactionIfNecessary(...); // BEGIN
Object result;
try {
result = invocation.proceedWithInvocation(); // the join point: YOUR method runs here
} catch (Throwable ex) {
completeTransactionAfterThrowing(txInfo, ex); // ROLLBACK (for most exceptions, see below)
throw ex;
} finally {
cleanupTransactionInfo(txInfo);
}
commitTransactionAfterReturning(txInfo); // COMMIT
return result;This is the begin/commit/rollback code from section 3, written once, in the framework, and wrapped around every @Transactional method by the proxy. Your methods contain only business logic.
Watch it happen
A controller and a bean with a transactional method. JdbcTemplate runs one SQL statement (it is covered in chapter 23); the table users has an id and a name:
java
@RestController
@RequestMapping("/api")
public class UserController {
@Autowired
User user;
@GetMapping("/updateuser")
public String updateUser() {
user.updateUser();
return "user is updated successfully";
}
}
@Component
public class User {
@Autowired
JdbcTemplate jdbcTemplate;
@Transactional
public void updateUser() {
System.out.println("UPDATE QUERY to update the user db values");
jdbcTemplate.update("insert into users(name) values ('a')");
}
}To see what the advice does, turn on Spring's transaction logging in application.properties:
properties
logging.level.org.springframework.transaction.interceptor=TRACE
logging.level.org.springframework.orm.jpa.JpaTransactionManager=DEBUGCall GET /api/updateuser:
text
Creating new transaction with name [com.example.demo.User.updateUser]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
Getting transaction for [com.example.demo.User.updateUser]
UPDATE QUERY to update the user db values
Completing transaction for [com.example.demo.User.updateUser]
Initiating transaction commit
Committing JPA transaction on EntityManager [SessionImpl(...)]Begin, your method, commit: the advice at work. (PROPAGATION_REQUIRED and ISOLATION_DEFAULT are the defaults of the two settings parts 2 and 3 explain.)
Now make it fail. A second method inserts a row, then throws:
java
@Transactional
public void updateUserFails() {
jdbcTemplate.update("insert into users(name) values ('b')");
throw new RuntimeException("something failed");
}text
Getting transaction for [com.example.demo.User.updateUserFails]
Completing transaction for [com.example.demo.User.updateUserFails] after exception: java.lang.RuntimeException: something failed
Initiating transaction rollback
Rolling back JPA transaction on EntityManager [SessionImpl(...)]Counting the rows afterwards gives 1, not 2: the insert of 'b' happened, and was undone by the rollback. That is atomicity.
Which exceptions roll back
By default, only unchecked exceptions (RuntimeException and its subclasses, and Error) roll back. A checked exception (Exception, IOException, ...) leaves the method, but the transaction is committed:
java
@Transactional
public void checkedFail() throws Exception {
jdbcTemplate.update("insert into users(name) values ('c')");
throw new Exception("checked");
}text
Completing transaction for [com.example.demo.User.checkedFail] after exception: java.lang.Exception: checked
Initiating transaction commitThe row 'c' stays. To roll back on checked exceptions too, say so:
java
@Transactional(rollbackFor = Exception.class)
public void checkedFailRb() throws Exception { ... }text
Creating new transaction with name [...checkedFailRb]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,-java.lang.Exception
...
Initiating transaction rollback(noRollbackFor does the opposite.)
The self-call trap
Because @Transactional is applied by a proxy, it only works when the call goes through the proxy, from another bean (chapter 12). A @Transactional method called from the same class gets no transaction:
java
public void outer() {
inner(); // this.inner(): bypasses the proxy
}
@Transactional
public void inner() {
System.out.println("in transaction: "
+ TransactionSynchronizationManager.isActualTransactionActive());
}Calling user.outer() from the controller prints in transaction: false. Call inner() from another bean, or make the outer method transactional.
Next: @Transactional hides a lot of machinery. Part 2 opens it up: the transaction context and the transaction manager behind the advice, declarative and programmatic transactions, and propagation, which decides what happens when one transactional method calls another.
Check yourself
- What is a critical section, and what goes wrong when four booking requests run it in parallel?
- Explain each letter of ACID with the ₹5 transfer from A to B.
- Why is writing begin/commit/rollback by hand a problem in a real application?
- Which two dependencies does
@Transactionalneed? Do you have to write@EnableTransactionManagementin Spring Boot? - What is the difference between
@Transactionalon a method and on a class? Which methods are never transactional? - Which AOP pieces implement
@Transactional, and in which class is the advice? - A
@Transactionalmethod inserts a row and then throws aRuntimeException. What happens to the row, and what does the log show? - The same method throws a checked
Exceptioninstead. What happens? How do you change that? - A method calls a
@Transactionalmethod of the same class. Does it run in a transaction? Why?
Answers
- Code that reads and modifies a shared resource. All four requests read "Available" and all four book the car: inconsistent data.
- Atomicity: debit and credit both happen or neither does. Consistency: 10 + 20 before; 5 + 25 or 10 + 20 after, never 5 + 20. Isolation: parallel transfers do not interfere. Durability: a committed transfer survives a crash.
- The same code is repeated around every method that writes data: verbose, duplicated, and mixed into the business logic.
- A data starter that brings transaction support (such as
spring-boot-starter-data-jpa) and a database driver. No, Spring Boot's auto-configuration adds it. - On a method, only that method; on a class, all its methods (with Spring Boot's class-based proxies, also
protectedand package-private ones).privatemethods are never transactional. - A pointcut matching
@Transactionaland an around advice,invokeWithinTransactioninTransactionInterceptor(from its parentTransactionAspectSupport). - The transaction is rolled back, so the row is removed. The log shows "Completing transaction ... after exception", "Initiating transaction rollback".
- The transaction is committed and the row stays. Use
@Transactional(rollbackFor = Exception.class). - No. The call goes to
this, not through the proxy, so the advice never runs.