Dev Learning Lab

The labs on this site need JavaScript to run. The same content is written out below.

Fundamentals › Spring

@Transactional

What does @Transactional group together?

@Transactional is a marker (an annotation) that you put above a method. The order() method in this lab saves to the database twice with jdbc.update (an order in the orders table and a payment in the payment table). Between the two saves there is a stock check, and if stock is 0, it throws an exception. With no marker, the database applies each save as it receives it, so even if an exception occurs, the earlier save stays. If you put @Transactional above order(), Spring treats the saves inside that method as one unit (a transaction). If the method ends normally, it applies all of them (commit), and if it ends with a runtime exception, it cancels all of them (rollback). We do not write the code that starts the unit and applies or cancels it. Spring does it before and after it calls order(). In this lab, you remove @Transactional or change the value put in stock, call the same order(), and look at the rows left in the two tables after the call ends.

Checked against real execution · Ran all 4 combinations with a real @Transactional proxy on Spring 7.0.9 and H2 2.4.240, and checked the row counts and the facts behind the step sentences

Concepts in this lab

@Transactional
This annotation is placed on a method or a class so that its scope runs as one transaction. The proxy starts a transaction before the method runs, commits if the method ends normally, and rolls back according to the rules if an exception occurs. You do not have to write the code that starts and ends the transaction yourself, so this is called a declarative transaction.
Transaction
A way to group several data changes into a single unit so that either all of them are applied (commit) or all of them are canceled (rollback). In a bank transfer, it prevents an intermediate state where only one of the withdrawal and the deposit is applied. It is the most basic means a database has for keeping data consistent.
Commit and rollback
A commit makes all the data changes in a transaction final in the database, and a rollback cancels all of those changes and returns to the state before the transaction started. A transaction ends in one of the two, and a change that has already been committed cannot be undone by a rollback. In Spring, when a method with @Transactional ends normally, Spring commits, and when it ends with an exception, Spring looks at the rollback rules and rolls back. Under the default rule, it rolls back when a RuntimeException or an Error is thrown out of the method, and it does not roll back for a checked exception. Without a transaction, each save statement is committed immediately by auto-commit, which is the default setting of a JDBC connection, so even if an exception occurs later, the earlier save cannot be rolled back.
Proxy
This is a stand-in object that receives calls in place of the real object. When it receives a call, it does some additional work and then passes the call on to the real object. Spring wraps a bean that needs a feature such as AOP or @Transactional in such an object and registers it in the container, so what other beans receive by injection is this stand-in, not the real object.

Try these

Running with the default settings

  1. The main of the program calls the order() method of the OrderService object that Spring created. This object is not the class we wrote as it is but an object of another class that Spring made (a proxy), so the call goes through the code of Spring before it reaches the first line of order(). The two tables, orders and payment, have no rows yet.
  2. Before it enters the order() method, Spring starts a transaction. That is because order() has @Transactional above it. From now on, the saves made inside order() belong to one transaction, and they are not final in the database until the transaction ends. The line that starts the transaction is not in the code we wrote.
  3. jdbc.update saves one row of an order (1, keyboard) to the orders table. This save is inside the transaction, so it is not final yet. If you open a separate connection to the same database and count orders, it has 0 rows.
  4. With int stock = 0, the condition of the next line, if (stock == 0), is true, so IllegalStateException("Out of stock") is thrown. The order() method ends with the exception without running the line that saves the payment.
  5. Spring sees that order() ended with an exception and cancels (rolls back) the whole transaction. The order row saved earlier is also undone and disappears from orders. IllegalStateException is a RuntimeException, so Spring canceled it by its default rule. The line that cancels is not in the code we wrote either, and the exception is passed on as it is to main, which called order().
  6. After order() ends, main queries the two tables. Rows in orders: 0. Rows in payment: 0. Neither the order nor the payment is left.

Result of this run

Code

@Service
public class OrderService {
    private final JdbcTemplate jdbc;

    public OrderService(JdbcTemplate jdbc) {
        this.jdbc = jdbc;
    }

    // Unit: the saves inside this method are all applied or all canceled
    @Transactional
    public void order() {
        jdbc.update("INSERT INTO orders VALUES (1, 'keyboard')");
        int stock = 0;
        if (stock == 0) {
            throw new IllegalStateException("Out of stock");
        }
        jdbc.update("INSERT INTO payment VALUES (1, 30000)");
    }
}

Assumptions of this model

This was checked against real execution of Spring Framework 7.0.9 on JDK 21, connected by JDBC to H2 2.4.240 (an in-memory database), and it was not checked against execution on MySQL. For all 4 combinations, the same source as the OrderService on the screen (with only the package and import lines and two measurement lines marked PROBE at the end added) was compiled, and a new Java process was started for each combination. The configuration class had @EnableTransactionManagement, which AnnotationConfigApplicationContext reads, an H2 DataSource (HikariCP), JdbcTemplate, and DataSourceTransactionManager (a subclass with only lines added to count starts, commits, and rollbacks), and OrderService was marked @Service so that Spring finds it by component scanning. Auto-commit was left at the JDBC default (on), and the database and the two tables (orders, payment) were created anew for each combination. What was checked is how order() ended (IllegalStateException and its message, or normal) and the number of rows in orders and payment counted by a separate query after the call ended. The facts that the step sentences rely on were also checked by execution: whether a transaction is active inside order() (active only when there is @Transactional), the class of the OrderService bean that main received (a CGLIB proxy class made by Spring when there is @Transactional, and the class we wrote as it is when there is not), the number of rows in orders counted over a separately connected database connection right after the first save (0 rows with @Transactional, 1 row without it), and the number of times the transaction manager started, committed, and rolled back (1 start and 1 rollback if it ended with an exception, 1 start and 1 commit if it ended normally, and all 0 without @Transactional). That the second save is not final inside the transaction, and that it is applied immediately without one, were derived from the value measured after the first save and from the final row counts, and the moment right after the second save was not measured separately. What the documentation defines is the Declarative Transaction Management section of Data Access in the Spring Framework 7.0 documentation (that declarative transactions are done through AOP), the section Understanding the Spring Framework's Declarative Transaction Implementation (that TransactionInterceptor and the transaction manager drive the transaction around the method call), the section Using @Transactional (that it must be turned on with @EnableTransactionManagement and that the default is the proxy mode, and that by default RuntimeException and Error are rolled back while checked exceptions are not), and the section Rolling Back a Declarative Transaction (that in the default settings only runtime exceptions are marked for rollback). However, the documentation does not describe in one paragraph the order of starting before the method, committing if normal, and rolling back if an exception, so that order is an observation based on the counts and the row numbers that the Probe measured. The code on the screen leaves out the package and import lines, and it does not show the main that calls order(), the CREATE TABLE that creates the tables, the database connection, and the Spring configuration. This lab covers only one exception, a RuntimeException being thrown, and does not cover checked exceptions, calls from inside the same class, or readOnly. Checked exceptions, rollbackFor, and propagation are covered in the "Transaction propagation" lab, and transactions themselves (including isolation levels) are covered in the "Transaction basics" lab.

Previous lab: JDBC · Next lab: Spring Boot

Concepts in this lab

What this lab shows

Settings

Code


      
    

Diagram

Run comparison

Try these