Skip to content
Chapter 13 · Spring Boot

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

  1. why transactions are needed: critical sections and ACID;
  2. what transaction code looks like by hand, and why that is a problem;
  3. @Transactional: setup and use;
  4. 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:

idstatus
1001Available
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 TRANSACTION

Two 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:

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

  2. 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 public methods only; with the class-based proxies Spring Boot uses (Spring 6), protected and package-private methods are covered too. private methods 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 invokeWithinTransaction of TransactionInterceptor (inherited from its parent class TransactionAspectSupport).

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=DEBUG

Call 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 commit

The 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 ​

  1. What is a critical section, and what goes wrong when four booking requests run it in parallel?
  2. Explain each letter of ACID with the ₹5 transfer from A to B.
  3. Why is writing begin/commit/rollback by hand a problem in a real application?
  4. Which two dependencies does @Transactional need? Do you have to write @EnableTransactionManagement in Spring Boot?
  5. What is the difference between @Transactional on a method and on a class? Which methods are never transactional?
  6. Which AOP pieces implement @Transactional, and in which class is the advice?
  7. A @Transactional method inserts a row and then throws a RuntimeException. What happens to the row, and what does the log show?
  8. The same method throws a checked Exception instead. What happens? How do you change that?
  9. A method calls a @Transactional method of the same class. Does it run in a transaction? Why?
Answers
  1. Code that reads and modifies a shared resource. All four requests read "Available" and all four book the car: inconsistent data.
  2. 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.
  3. The same code is repeated around every method that writes data: verbose, duplicated, and mixed into the business logic.
  4. 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.
  5. On a method, only that method; on a class, all its methods (with Spring Boot's class-based proxies, also protected and package-private ones). private methods are never transactional.
  6. A pointcut matching @Transactional and an around advice, invokeWithinTransaction in TransactionInterceptor (from its parent TransactionAspectSupport).
  7. The transaction is rolled back, so the row is removed. The log shows "Completing transaction ... after exception", "Initiating transaction rollback".
  8. The transaction is committed and the row stays. Use @Transactional(rollbackFor = Exception.class).
  9. No. The call goes to this, not through the proxy, so the advice never runs.