Spring Boot @Transactional Annotation

🚀 Introduction: What is @Transactional in Spring Boot?

The @Transactional annotation in Spring Boot is used to manage database transactions automatically. It ensures that database operations such as save, update, and delete are executed as a single unit, maintaining data integrity.

Key Features of @Transactional:
✔ Ensures atomicity (all operations succeed or fail together).
Rolls back transactions automatically on exceptions.
✔ Supports different propagation behaviors (nested, mandatory, required).
✔ Works with Spring Data JPA and Hibernate.

📌 In this guide, you’ll learn:
How @Transactional works in Spring Boot.
How to use it for rollback, propagation, and isolation levels.
Best practices for writing transactional code.

1️⃣ Why Use @Transactional?

Consider a scenario where you need to save two related entities in a database. If the second save operation fails, the first one should also be rolled back to maintain data consistency.

📌 Example: Without @Transactional, Inconsistent Data May Occur

public void registerUser(String name, String email) {
    userRepository.save(new User(name, email)); // Saved successfully
    
    if (email.contains("invalid")) {
        throw new RuntimeException("Invalid email!"); // Exception occurs
    }

    logRepository.save(new UserLog(email, "User registered")); // Not executed
}

📌 Problem:

  • The User record is saved before the exception occurs, but the UserLog entry is not saved.
  • This results in inconsistent database state.

Solution: Wrap the entire method with @Transactional to ensure rollback.

2️⃣ Basic Example: Using @Transactional for Atomicity

📌 Example: Ensuring Atomicity with @Transactional

1. Service Layer (UserService.java)

@Service
public class UserService {

    private final UserRepository userRepository;
    private final UserLogRepository userLogRepository;

    public UserService(UserRepository userRepository, UserLogRepository userLogRepository) {
        this.userRepository = userRepository;
        this.userLogRepository = userLogRepository;
    }

    @Transactional // Ensures both operations are successful or rolled back
    public void registerUser(String name, String email) {
        userRepository.save(new User(name, email));

        if (email.contains("invalid")) {
            throw new RuntimeException("Invalid email!"); // Forces rollback
        }

        userLogRepository.save(new UserLog(email, "User registered"));
    }
}

2. Repository Layer (UserRepository.java)

public interface UserRepository extends JpaRepository<User, Long> {
}

📌 Scenario 1: Valid Email (registerUser("Ramesh", "ramesh@example.com"))
User and log entry are both saved.

📌 Scenario 2: Invalid Email (registerUser("Suresh", "invalid-email"))
User and log entry are both rolled back (data remains consistent).

Ensures database consistency by rolling back all changes when an exception occurs.

3️⃣ @Transactional and Rollback Behavior

By default, @Transactional rolls back on unchecked exceptions (RuntimeException, Error), but not on checked exceptions (Exception).

📌 Example: Rollback on Checked Exceptions (SQLException)

@Transactional(rollbackFor = Exception.class) // Ensures rollback for all exceptions
public void registerUser(String name, String email) throws SQLException {
    userRepository.save(new User(name, email));
    
    if (email.contains("invalid")) {
        throw new SQLException("Database error!"); // Checked exception
    }

    userLogRepository.save(new UserLog(email, "User registered"));
}

Ensures rollback for both checked and unchecked exceptions.

4️⃣ Understanding @Transactional Propagation Levels

Propagation determines how transactions interact when multiple methods with @Transactional are called.

Propagation Type Behavior
REQUIRED (Default) Uses an existing transaction or creates a new one if none exists.
REQUIRES_NEW Always creates a new transaction, suspending the current one.
MANDATORY Must run within an existing transaction, otherwise throws an exception.
NESTED Runs within a nested transaction inside the existing one.
SUPPORTS Uses the current transaction if available, otherwise runs without one.
NOT_SUPPORTED Executes the method outside of any transaction.

📌 Example: Using REQUIRES_NEW to Ensure Logs Are Always Saved

@Transactional
public void registerUser(String name, String email) {
    userRepository.save(new User(name, email));

    try {
        saveUserLog(email);
    } catch (Exception e) {
        // Log the error but do not affect user registration
    }
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveUserLog(String email) {
    userLogRepository.save(new UserLog(email, "User registered"));
}

If registerUser fails, UserLog is still saved because it runs in a separate transaction.

5️⃣ Setting Isolation Levels for Concurrency Control

Isolation levels prevent dirty reads, non-repeatable reads, and phantom reads in concurrent transactions.

Isolation Level Behavior
READ_COMMITTED (Default) Prevents dirty reads but allows non-repeatable reads.
REPEATABLE_READ Prevents dirty and non-repeatable reads but allows phantom reads.
SERIALIZABLE Full transaction isolation (slowest but safest).

📌 Example: Using Isolation.READ_COMMITTED to Prevent Dirty Reads

@Transactional(isolation = Isolation.READ_COMMITTED)
public void processTransaction() {
    // Transaction code here
}

Ensures only committed data is read by concurrent transactions.

6️⃣ Using @Transactional with Spring Data JPA Queries

Spring Data JPA methods automatically run inside transactions, but you can override defaults.

📌 Example: Ensuring a Custom Query Runs in a Transaction

@Transactional
@Modifying
@Query("UPDATE User u SET u.email = :email WHERE u.id = :id")
void updateUserEmail(@Param("id") Long id, @Param("email") String email);

@Modifying queries need @Transactional to execute updates safely.

🎯 Summary: Best Practices for Using @Transactional

Use @Transactional at the service layer, not controllers.
Ensure rollback for checked exceptions using rollbackFor = Exception.class.
Use Propagation.REQUIRES_NEW for independent transactions.
Set appropriate isolation levels (READ_COMMITTED, SERIALIZABLE).
Use @Modifying with @Transactional for update and delete queries.
Log exceptions instead of swallowing them inside transactions.

🚀 Following these best practices ensures reliable, consistent database transactions in Spring Boot!

Comments

Spring Boot 3 Paid Course Published for Free
on my Java Guides YouTube Channel

Subscribe to my YouTube Channel (165K+ subscribers):
Java Guides Channel

Top 10 My Udemy Courses with Huge Discount:
Udemy Courses - Ramesh Fadatare