Skip to content

Spring Boot @Transactional Annotation Part 1

Real World Analogy: The Bank Transfer Problem

Imagine you are transferring money from your savings account to a friend's account. The bank's system needs to do two things: subtract money from your account and add it to your friend's account. Now what if the power goes out after the subtraction but before the addition? You lose money that never reaches your friend.

This is exactly the kind of problem that transactions solve. A transaction says: either both operations succeed completely, or neither of them happens at all. The bank either completes the transfer or reverses it entirely. There is no in between.

In software, we face the same challenge constantly. Whenever multiple database operations must succeed or fail together, we need transactions.


What is a Critical Section?

Before diving into transactions, you need to understand critical section. A critical section is a code segment where shared resources are being accessed and modified.

Think of a cab booking application. The database has a car with ID 10001 and its status is currently AVAILABLE. The booking logic checks availability and then updates status to BOOKED. This check and update together form a critical section because:

  • Multiple users might try to book the same car simultaneously
  • If four users all read AVAILABLE at the same time, all four might try to update to BOOKED
  • All four could receive a confirmation for the same cab

Without proper handling of the critical section, data inconsistency occurs. Transactions are the solution.


ACID Properties: The Foundation of Transactions

Every time you studied databases in college, ACID came up. Now let us understand each property deeply so you know why they matter in Spring Boot.

A — Atomicity

Atomicity says: if any operation in a transaction fails, the entire transaction rolls back.

Example:

  • Account A has ₹10, Account B has ₹20
  • Transaction: debit A by ₹5, credit B by ₹5
  • If debit succeeds (A now has ₹5) but credit fails — atomicity rolls back the debit too
  • A goes back to ₹10, B stays at ₹20

No partial success is allowed.

C — Consistency

Consistency says: before and after a transaction, the database must be in a consistent state.

If the transfer completes successfully, A has ₹5 and B has ₹25 — still consistent. If it fails and rolls back, A has ₹10 and B has ₹20 — also consistent. What is NOT allowed is A having ₹5 and B still having ₹20, which would be an inconsistent state where money vanished from the system.

I — Isolation

Isolation says: even if multiple transactions run in parallel, they should not interfere with each other.

Each transaction should feel like it is running alone in an isolated environment. Internally, transactions use locking to create a proper sequence. From the outside it looks like everything runs in parallel, but inside the database ensures order. Only one transaction can hold a lock on a row at a time.

D — Durability

Durability says: once a transaction commits, the data is permanently persisted. Even if the system crashes immediately after a commit, that data must not be lost.

All four properties together ensure reliability in financial applications, booking systems, inventory management, and any other system where data integrity matters.


The Problem With Manual Transaction Management

Suppose your application has a thousand service methods that touch the database. For each one you would need to write:

java
// Manual transaction management — very verbose
connection.setAutoCommit(false);           // begin transaction
try {
    // your actual business logic
    updateAccountA(connection, -5);
    updateAccountB(connection, +5);
    connection.commit();                   // all success: commit
} catch (Exception e) {
    connection.rollback();                 // any failure: roll back
} finally {
    connection.setAutoCommit(true);
}

Your actual business logic is just those two update lines. Everything else is boilerplate that you must copy into every single method across every class. If you have 100 classes and 10 methods each, that is 1000 places with identical transaction scaffolding code.

This is where @Transactional in Spring Boot saves you.


How Spring Boot's @Transactional Works

Dependencies Required

To use @Transactional, you need two dependencies in your pom.xml:

xml
<!-- Spring Data JPA provides @Transactional support for relational databases -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<!-- Database driver — change this based on which database you use -->
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

Also add connection settings in application.properties:

properties
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.username=sa
spring.datasource.password=

Note: If you use a NoSQL database that supports transactions, you use a different library. The annotation @Transactional itself comes from spring-tx, but the right implementation is wired based on your data source.

Enabling Transaction Management

java
@SpringBootApplication
@EnableTransactionManagement  // optional — Spring Boot adds this automatically
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

Spring Boot auto configures transaction management. You do not need @EnableTransactionManagement explicitly, but it is good to know it exists.

Applying @Transactional

You can apply it at the class level or the method level.

java
@Service
@Transactional  // applies to ALL public methods in this class
public class UserService {

    public void updateUser(User user) {
        // this method is transactional
    }

    public void deleteUser(Long id) {
        // this method is also transactional
    }

    private void helperMethod() {
        // private methods are NOT covered by @Transactional at class level
    }
}
java
@Service
public class OrderService {

    @Transactional  // only this specific method is transactional
    public void placeOrder(Order order) {
        // transactional
    }

    public void getOrder(Long id) {
        // NOT transactional
    }
}

Key rule: Class level @Transactional covers all public methods, not private ones.


How @Transactional Works Internally: AOP Under the Hood

This is where understanding Aspect Oriented Programming (AOP) becomes essential. Spring Boot's transaction management is built entirely on top of AOP.

The Big Picture

When you write:

java
@Service
public class UserService {

    @Transactional
    public void updateUser(Long userId, String newName) {
        // your business logic here
    }
}

You only write the business logic. The begin, commit, and rollback operations are completely absent from your code. So where do they go?

They are inside a Spring AOP Aspect called TransactionInterceptor.

The Point Cut Expression

When Spring scans your application, it uses a point cut expression similar to:

@within(org.springframework.transaction.annotation.Transactional)

This expression scans all classes and all methods. Whenever it finds a method annotated with @Transactional, it registers a match. Every matched method will have an advice run around it.

The Advice: TransactionInterceptor

The advice is an around advice (the most powerful type). It lives inside the TransactionInterceptor class. Specifically, inside a method called invokeWithinTransaction.

Here is what the framework does internally (simplified):

java
// Inside TransactionInterceptor — this is Spring's code, not yours
Object invokeWithinTransaction(Method method, Object target, ...) throws Throwable {
    // Step 1: Begin the transaction
    TransactionStatus txStatus = txManager.getTransaction(txAttr);

    Object retVal;
    try {
        // Step 2: Invoke YOUR method (the join point)
        retVal = invocation.proceed();  // your business logic runs here

        // Step 3: All success — commit
        txManager.commit(txStatus);

    } catch (Throwable ex) {
        // Step 4: Any exception — roll back
        completeTransactionAfterThrowing(txInfo, ex);  // calls txManager.rollback()
        throw ex;
    }

    return retVal;
}

You write the two update lines. Spring wraps them in begin, commit, and rollback. This is the power of AOP in transaction management.


A Complete Working Example

Controller

java
@RestController
@RequestMapping("/api")
public class UserController {

    @Autowired
    private UserService userService;

    // Note: should be PostMapping for updates — using GetMapping here for easy testing
    @GetMapping("/update-user")
    public String updateUser() {
        userService.updateUser();
        return "User updated successfully";
    }
}

Service — Success Case

java
@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    @Transactional
    public void updateUser() {
        // Spring starts a transaction before entering this method

        // Operation 1: update user name
        userRepository.updateUserName(1L, "Alice");

        // Operation 2: update user email
        userRepository.updateUserEmail(1L, "alice@example.com");

        // If we reach here without exception, Spring commits the transaction
        // Both updates are permanently saved
    }
}

Service — Failure Case (Automatic Rollback)

java
@Service
public class PaymentService {

    @Autowired
    private AccountRepository accountRepository;

    @Transactional
    public void transferFunds(Long fromId, Long toId, Double amount) {
        // Step 1: debit sender
        accountRepository.debit(fromId, amount);

        // Step 2: simulate a failure (e.g., recipient account not found)
        if (toId == null) {
            throw new RuntimeException("Recipient account not found!");
            // Spring catches this exception and rolls back Step 1 automatically
            // The debit is reversed — no money is lost
        }

        // Step 3: credit recipient (only reached if no exception above)
        accountRepository.credit(toId, amount);

        // Spring commits if we reach here
    }
}

When a RuntimeException is thrown inside a @Transactional method, Spring automatically calls rollback(). All changes made so far in that transaction are reversed.


What Happens Step by Step at Runtime

  1. Application starts — Spring scans all beans. It finds @Transactional methods using the point cut expression. It creates a proxy around those beans.

  2. API is called — the request reaches the proxy, not your actual service class directly.

  3. Proxy interceptsTransactionInterceptor.invokeWithinTransaction runs.

  4. Transaction beginstxManager.getTransaction(...) is called, which internally does BEGIN TRANSACTION at the database level.

  5. Your method runs — all your SQL queries execute within the open transaction.

  6. Success pathtxManager.commit(...) is called. Changes are permanently written to the database.

  7. Failure path — if a RuntimeException or Error is thrown, txManager.rollback(...) is called. All changes in this transaction are reversed.


Transaction Proxy: Why Private Methods Don't Work

Since Spring uses a proxy to intercept method calls, there is an important limitation:

java
@Service
public class OrderService {

    @Transactional
    public void placeOrder() {
        // This is called through the Spring proxy — works correctly
        saveOrderToDb();
        sendConfirmationEmail();
    }

    @Transactional
    private void saveOrderToDb() {
        // WARNING: This will NOT be transactional!
        // Private methods are not intercepted by the proxy.
        // The proxy calls the real object's public method, which calls this private
        // method directly — bypassing the proxy entirely.
    }
}

Rule: @Transactional on private methods is silently ignored. Always apply it to public methods.

Also, calling a @Transactional method from within the same class (self invocation) bypasses the proxy and the transaction. Call it from a different bean.


Summary

ConceptKey Point
Critical SectionCode that accesses shared resources; needs protection
ACIDAtomicity, Consistency, Isolation, Durability — guaranteed by transactions
@TransactionalDeclarative way to manage transactions in Spring Boot
AOP IntegrationSpring wraps your method with begin, commit, rollback using TransactionInterceptor
Class vs MethodClass level applies to all public methods; method level applies only to that method
Private Methods@Transactional has no effect on private methods — proxy cannot intercept them
Rollback TriggerRuntimeException and Error trigger automatic rollback by default

Interview Questions

Q1: What is ACID and why is it important in Spring transactions?

ACID stands for Atomicity, Consistency, Isolation, and Durability. Atomicity ensures all operations in a transaction succeed or all roll back. Consistency ensures the database moves from one valid state to another. Isolation ensures parallel transactions do not interfere. Durability ensures committed data survives crashes. Spring transactions guarantee ACID behavior for your database operations.

Q2: How does @Transactional work internally in Spring Boot?

Spring Boot uses AOP (Aspect Oriented Programming). When it finds a method annotated with @Transactional, it creates a proxy around the bean. When the method is called, the proxy intercepts the call and runs TransactionInterceptor.invokeWithinTransaction, which begins a transaction, invokes the actual method, and then commits on success or rolls back on exception.

Q3: What happens when an exception is thrown inside a @Transactional method?

By default, Spring rolls back the transaction for unchecked exceptions (RuntimeException and its subclasses) and Error. For checked exceptions it does NOT roll back unless you explicitly specify @Transactional(rollbackFor = SomeCheckedException.class).

Q4: Can you apply @Transactional to a private method?

No. Spring uses a proxy to intercept method calls, and proxies only work for public methods. Applying @Transactional to a private method is silently ignored — the annotation has no effect.

Q5: What is self invocation and why does it break @Transactional?

Self invocation is when a method in a class calls another method in the same class. Since Spring's proxy sits in front of the bean, a direct internal call bypasses the proxy. If the called method has @Transactional, the proxy never runs, so no transaction is started. To fix this, move the called method to a separate bean.

Q6: What is the difference between class level and method level @Transactional?

Class level applies the annotation to all public methods in the class. Method level applies it only to the specific method. Method level annotation overrides class level if both are present. Private methods are never covered regardless of where the annotation is placed.

Q7: What is @EnableTransactionManagement and is it required?

@EnableTransactionManagement activates annotation driven transaction management. In a Spring Boot application it is auto configured and not required explicitly. However, if Spring Boot somehow does not auto configure it, this annotation ensures @Transactional annotations are processed correctly.