How Spring’s @Transactional Works Under the Hood and Build Your Own Transaction Annotation

This article explains Spring Boot’s @Transactional implementation, covers transaction isolation levels and propagation, walks through the core source code of TransactionInterceptor and PlatformTransactionManager, and guides you step‑by‑step to create a custom annotation that rolls back on specified exceptions using JDBC and AOP.

ITFLY8 Architecture Home
ITFLY8 Architecture Home
ITFLY8 Architecture Home
How Spring’s @Transactional Works Under the Hood and Build Your Own Transaction Annotation

1. Overview

Developers often use Spring Boot’s @Transactional for transaction management without knowing how it works. This article examines the source code of @Transactional, explains its implementation, and then builds a similar custom annotation to deepen understanding.

2. Transaction Fundamentals

Before diving into the code, we review basic transaction concepts.

2.1 Isolation Levels

Isolation prevents problems in concurrent transactions:

Dirty Read : A transaction reads uncommitted changes that may be rolled back.

Lost Update : Two transactions modify the same row, causing one update to be lost.

Non‑repeatable Read : A row’s value changes between two reads within the same transaction.

Phantom Read : New rows appear between two reads of a range query.

Spring defines five constants representing isolation levels (image omitted).

2.2 Propagation Mechanism

Spring’s propagation rules determine how nested method calls share or create transactions. Seven propagation behaviors are defined (image omitted).

3. How Spring Handles Rollback

After reviewing transaction theory, we explore how Spring Boot uses TransactionInterceptor and PlatformTransactionManager to manage commit and rollback. TransactionInterceptor intercepts method execution, decides whether to start a transaction, and catches exceptions.

// simplified TransactionInterceptor logic
public Object invoke(MethodInvocation invocation) throws Throwable {
    Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
    return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
}

The core of invokeWithinTransaction looks like:

protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
        final InvocationCallback invocation) throws Throwable {
    Object retVal;
    try {
        retVal = invocation.proceedWithInvocation();
    } catch (Throwable ex) {
        completeTransactionAfterThrowing(txInfo, ex);
        throw ex;
    } finally {
        cleanupTransactionInfo(txInfo);
    }
    commitTransactionAfterReturning(txInfo);
    return retVal;
}

When an exception occurs, completeTransactionAfterThrowing decides whether to roll back:

protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
    if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
        txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
    } else {
        txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
    }
}

For JDBC, DataSourceTransactionManager performs the actual rollback:

@Override
protected void doRollback(DefaultTransactionStatus status) {
    DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
    Connection con = txObject.getConnectionHolder().getConnection();
    try {
        con.rollback();
    } catch (SQLException ex) {
        throw new TransactionSystemException("Could not roll back JDBC transaction", ex);
    }
}

4. Implementing a Custom Transaction Annotation

With the theory clear, we create our own annotation that rolls back on specified exceptions using plain JDBC and Spring AOP.

4.1 Add Dependencies

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

4.2 Define the Annotation

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface MyTransaction {
    // specify exception types that trigger rollback
    Class<? extends Throwable>[] rollbackFor() default {};
}

4.3 Connection Holder

A helper class binds a JDBC Connection to the current thread.

@Component
public class DataSourceConnectHolder {
    @Autowired
    DataSource dataSource;
    ThreadLocal<Connection> resources = new NamedThreadLocal<>("Transactional resources");

    public Connection getConnection() {
        Connection con = resources.get();
        if (con != null) return con;
        try {
            con = dataSource.getConnection();
            con.setAutoCommit(false);
        } catch (SQLException e) { e.printStackTrace(); }
        resources.set(con);
        return con;
    }

    public void cleanHolder() {
        Connection con = resources.get();
        if (con != null) {
            try { con.close(); } catch (SQLException e) { e.printStackTrace(); }
        }
        resources.remove();
    }
}

4.4 Transaction Aspect

The aspect intercepts methods annotated with @MyTransaction, executes them, and rolls back or commits based on the declared exception types.

@Aspect
@Component
public class MyTransactionAopHandler {
    @Autowired
    DataSourceConnectHolder connectHolder;
    Class<? extends Throwable>[] es;

    @org.aspectj.lang.annotation.Pointcut("@annotation(luozhou.top.annotion.MyTransaction)")
    public void Transaction() {}

    @Around("Transaction()")
    public Object TransactionProceed(ProceedingJoinPoint proceed) throws Throwable {
        Object result = null;
        Method method = ((MethodSignature) proceed.getSignature()).getMethod();
        MyTransaction transaction = method.getAnnotation(MyTransaction.class);
        if (transaction != null) es = transaction.rollbackFor();
        try {
            result = proceed.proceed();
        } catch (Throwable throwable) {
            completeTransactionAfterThrowing(throwable);
            throw throwable;
        }
        doCommit();
        return result;
    }

    private void doRollBack() {
        try { connectHolder.getConnection().rollback(); }
        catch (SQLException e) { e.printStackTrace(); }
        finally { connectHolder.cleanHolder(); }
    }

    private void doCommit() {
        try { connectHolder.getConnection().commit(); }
        catch (SQLException e) { e.printStackTrace(); }
        finally { connectHolder.cleanHolder(); }
    }

    private void completeTransactionAfterThrowing(Throwable throwable) {
        if (es != null && es.length > 0) {
            for (Class<? extends Throwable> e : es) {
                if (e.isAssignableFrom(throwable.getClass())) { doRollBack(); return; }
            }
        }
        doCommit();
    }
}

4.5 Testing the Annotation

Create a simple table tb_test and a service that inserts two rows inside a method annotated with @MyTransaction. The method deliberately throws an exception to verify rollback behavior.

CREATE TABLE `tb_test` (
  `id` int(11) NOT NULL,
  `email` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

Service implementation:

@Service
public class MyTransactionTest implements TestService {
    @Autowired
    DataSourceConnectHolder holder;

    @MyTransaction(rollbackFor = NullPointerException.class)
    @Override
    public void saveTest(int id) {
        saveWithParameters(id, "[email protected]");
        saveWithParameters(id + 10, "[email protected]");
        int aa = id / 0; // triggers ArithmeticException
    }

    private void saveWithParameters(int id, String email) {
        String sql = "insert into tb_test values(?,?)";
        Connection connection = holder.getConnection();
        try (PreparedStatement stmt = connection.prepareStatement(sql)) {
            stmt.setInt(1, id);
            stmt.setString(2, email);
            stmt.executeUpdate();
        } catch (SQLException e) { e.printStackTrace(); }
    }
}

Unit test:

@SpringBootTest
@RunWith(SpringRunner.class)
public class SpringTransactionApplicationTests {
    @Autowired
    private TestService service;

    @Test
    public void contextLoads() throws SQLException {
        service.saveTest(1);
    }
}

Running the test with rollbackFor = NullPointerException.class does not roll back the ArithmeticException, so the rows remain. Changing the annotation to roll back on ArithmeticException causes the inserts to be undone, confirming the custom annotation works.

5. Summary

The article revisits transaction concepts—dirty read, lost update, non‑repeatable read, phantom read—and explains why isolation levels (read uncommitted, read committed, repeatable read, serializable) are needed. Spring enhances transactions with propagation mechanisms to manage nested method calls. The core of Spring’s @Transactional relies on TransactionInterceptor to intercept methods, evaluate exceptions, and delegate commit or rollback to a concrete PlatformTransactionManager such as DataSourceTransactionManager. Finally, we demonstrated how to implement a custom @MyTransaction annotation using JDBC and Spring AOP to roll back on user‑specified exceptions.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

transactionaopspringJDBCannotation
ITFLY8 Architecture Home
Written by

ITFLY8 Architecture Home

ITFLY8 Architecture Home - focused on architecture knowledge sharing and exchange, covering project management and product design. Includes large-scale distributed website architecture (high performance, high availability, caching, message queues...), design patterns, architecture patterns, big data, project management (SCRUM, PMP, Prince2), product design, and more.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.