Posted on: January 19, 2025 Posted by: rahulgite Comments: 0

Spring provides robust support for managing database transactions, ensuring consistency and integrity. Below are the concepts, configurations, and examples to understand transactions in Spring Boot.


1. Transactions in Spring

A transaction is a unit of work that is either completed entirely or not at all. Spring’s transaction management ensures data consistency even in complex scenarios.

Key Concepts:

  1. Atomicity: All steps in a transaction are treated as a single unit.
  2. Consistency: Data integrity is maintained during and after transactions.
  3. Isolation: Transactions are executed independently.
  4. Durability: Once a transaction is committed, it remains persistent.

Annotations Used:

  • @Transactional: Declares a method or class transactional.

Class-Level vs Method-Level Transactions

  • Class-Level @Transactional: Applies the same transaction configuration to all methods in the class.
  • Method-Level @Transactional: Allows specific transaction configurations for individual methods, overriding the class-level configuration if present.

Example:

@Service
@Transactional(isolation = Isolation.READ_COMMITTED)
public class TransactionService {
    public void defaultTransaction() {
        // Uses class-level transaction configuration
    }
    @Transactional(isolation = Isolation.SERIALIZABLE)
    public void specificTransaction() {
        // Overrides class-level configuration with method-specific settings
    }
}

2. Isolation Levels in Transactions

Isolation levels define how transactions interact with each other. Spring supports the following isolation levels:

πŸ”„ 2.1. Isolation Levels (with Example)

Let’s assume we have a table accounts:

idnamebalance
1Alice1000
2Bob500

1️⃣ READ_UNCOMMITTED – ❗ Allows Dirty Reads

You can read data from other transactions even if it hasn’t been committed.

Example:

  • Transaction A updates Alice’s balance to 2000 but doesn’t commit yet.
  • Transaction B reads Alice’s balance and sees 2000.
  • Transaction A rolls back.

➑ Now B has seen invalid data (dirty read).

Real-world scenario: An admin dashboard showing the approximate number of active users or total page views for the day. If a few transactions fail and roll back, the counter might be temporarily off by a tiny fraction, but nobody will notice or care. It keeps the database blazing fast for these heavy aggregation queries.


2️⃣ READ_COMMITTED – βœ… Prevents Dirty Reads

You can only read committed data, but the value might change if read again.

Example:

  • Transaction A updates Alice’s balance to 2000 but doesn’t commit.
  • Transaction B tries to read Alice’s balance.
  • B waits until A commits (or rolls back).

But:

  • If B reads twice, and A commits in between, B sees two different values.

➑ Prevents dirty reads, but allows non-repeatable reads.

Real-world scenario: A resident opens an app to read the latest community notice board or update their phone number. You want them to see officially saved data, but if an admin edits the notice five minutes later, it is completely fine if the user doesn’t see the update until they pull to refresh the app.


3️⃣ REPEATABLE_READ – βœ… Prevents Dirty & Non-Repeatable Reads

Once data is read, it stays the same within that transaction.

Example:

  • Transaction A reads Alice’s balance = 1000.
  • Transaction B updates Alice’s balance to 2000 and commits.
  • Transaction A reads again β€” still sees 1000, because repeatable read is enforced.

➑ Prevents:

  • ❌ Dirty reads
  • ❌ Non-repeatable reads
    βœ… But phantom reads may still occur (e.g., new rows added).
  • Real-world scenario: A scheduled job running on the 1st of the month to calculate maintenance bills for all flats in a building. If the script is halfway through generating invoices and an admin suddenly updates the base maintenance rate, you don’t want half the invoices using the old rate and half using the new rate. You need the rate to remain “repeatable” and unchanged throughout the entire lifecycle of that specific billing transaction.

4️⃣ SERIALIZABLE – 🧱 Highest Isolation, Lowest Concurrency

Ensures complete isolation by executing transactions as if one at a time.

Example:

  • Transaction A reads all rows with balance > 500.
  • Transaction B wants to insert a new row with balance = 600.
  • B must wait for A to commit/rollback.

➑ Prevents:

  • Dirty reads βœ…
  • Non-repeatable reads βœ…
  • Phantom reads βœ…

But:

  • 🐒 It slows down performance due to strict locking.

Real-world scenario: Booking a limited resource, like reserving a shared clubhouse for a party. If two people tap “Book Now” at the exact same millisecond, SERIALIZABLE ensures the database processes one transaction completely before even looking at the other. This guarantees the system never accidentally double-books the venue.


βœ… Spring Boot Example

You can set transaction isolation in Spring using @Transactional:

import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;

@Service
public class AccountService {

@Transactional(isolation = Isolation.READ_COMMITTED)
public void updateBalance() {
// This method runs with READ_COMMITTED isolation
}
}

Available Isolation levels:

Isolation.READ_UNCOMMITTED
Isolation.READ_COMMITTED
Isolation.REPEATABLE_READ
Isolation.SERIALIZABLE

🧠 Summary Table

IsolationDirty ReadNon-repeatable ReadPhantom ReadPerformance
READ_UNCOMMITTEDβœ… Yesβœ… Yesβœ… YesπŸ”Ό Fastest
READ_COMMITTED❌ Noβœ… Yesβœ… YesπŸ”Ό Good
REPEATABLE_READ❌ No❌ Noβœ… Yesβš–οΈ Balanced
SERIALIZABLE❌ No❌ No❌ NoπŸ”½ Slowest

Let me know if you want a real Spring Boot + DB simulation of this in a project or test case setup!

Example of Isolation Levels:

@Service
public class IsolationService {
    @Transactional(isolation = Isolation.READ_COMMITTED)
    public void performReadCommittedTransaction() {
        // Business logic
    }
    @Transactional(isolation = Isolation.SERIALIZABLE)
    public void performSerializableTransaction() {
        // Business logic
    }
}

3. Propagation Types in Transactions

Propagation types determine how transactions behave when methods are nested.

3.1. Propagation Types:

  1. REQUIRED: Uses the current transaction or creates a new one.
  2. REQUIRES_NEW: Suspends the current transaction and starts a new one.
  3. NESTED: Creates a nested transaction within the current transaction.
  4. SUPPORTS: Executes within a transaction if available.
  5. NOT_SUPPORTED: Executes without a transaction, suspending the current one.
  6. MANDATORY: Requires an active transaction; throws an exception otherwise.
  7. NEVER: Ensures that no transaction is active.

3.1. Propagation Types (Spring @Transactional)

Let’s assume we have two service methods:

  • ServiceA.methodA() β†’ outer method
  • ServiceB.methodB() β†’ called inside methodA

We’ll illustrate how transactions behave between these methods.


1️⃣ REQUIRED (default)

Joins existing transaction if present; otherwise starts a new one.

@Transactional(propagation = Propagation.REQUIRED)
public void methodB() { ... }

Example:

  • methodA() has a transaction.
  • methodB() joins the same transaction.
  • If methodB() fails, the entire transaction rolls back.

βœ… Use case: Most common.


2️⃣ REQUIRES_NEW

Always starts a new transaction. Suspends any existing one.

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() { ... }

Example:

  • methodA() has a transaction.
  • methodB() runs in a new, independent transaction.
  • If methodB() fails, only methodB() is rolled back.
  • methodA() continues unless it fails too.

βœ… Use case: Logging, audit trails, sending notifications even if main fails.


3️⃣ NESTED

Creates a nested transaction within an existing one, using savepoints.

@Transactional(propagation = Propagation.NESTED)
public void methodB() { ... }

Example:

  • methodA() has a transaction.
  • methodB() creates a savepoint inside the same transaction.
  • If methodB() fails, only its part rolls back (if handled), and methodA() can still continue.

🧠 Note: Supported only with JDBC & specific transaction managers (e.g., DataSourceTransactionManager).


4️⃣ SUPPORTS

Participates in a transaction if one exists. Else, runs non-transactionally.

@Transactional(propagation = Propagation.SUPPORTS)
public void methodB() { ... }

Example:

  • If methodA() has a transaction β†’ methodB() joins.
  • If no transaction β†’ methodB() runs without one.

βœ… Use case: Read operations where transaction is optional.


5️⃣ NOT_SUPPORTED

Always runs without a transaction. Suspends any existing transaction.

@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void methodB() { ... }

Example:

  • methodA() has a transaction.
  • methodB() executes outside of it.
  • Transaction in methodA() is suspended during methodB().

βœ… Use case: Non-transactional operations like file I/O, email sending, etc.


6️⃣ MANDATORY

Must run inside a transaction. Throws exception if none exists.

@Transactional(propagation = Propagation.MANDATORY)
public void methodB() { ... }

Example:

  • If methodA() is transactional β†’ OK.
  • If methodA() is not transactional β†’ methodB() throws TransactionRequiredException.

βœ… Use case: When a method must be part of an outer transaction.


7️⃣ NEVER

Must not run inside a transaction. Throws exception if one exists.

@Transactional(propagation = Propagation.NEVER)
public void methodB() { ... }

Example:

  • If methodA() has a transaction β†’ ❌ methodB() fails.
  • If methodA() is non-transactional β†’ βœ… methodB() runs fine.

βœ… Use case: When a method must avoid transactions, like legacy APIs or constraints.


πŸ” Summary Table

PropagationJoins Existing TXCreates New TXThrows If TX ExistsThrows If No TX
REQUIREDβœ… Yesβœ… Yes❌ No❌ No
REQUIRES_NEW❌ Suspendsβœ… Yes❌ No❌ No
NESTEDβœ… Yes (savepoint)βœ… Nested TX❌ No❌ No
SUPPORTSβœ… Yes❌ No❌ No❌ No
NOT_SUPPORTED❌ Suspends❌ No❌ No❌ No
MANDATORYβœ… Yes❌ No❌ Noβœ… Yes
NEVER❌ No❌ Noβœ… Yes❌ No

Example of Propagation Types:

@Service
public class PropagationService {
    @Transactional(propagation = Propagation.REQUIRED)
    public void requiredTransaction() {
        // Business logic
    }
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void requiresNewTransaction() {
        // Business logic
    }
    @Transactional(propagation = Propagation.NESTED)
    public void nestedTransaction() {
        // Business logic
    }
}

4. Cascade Types in Spring

Cascade types define the operations that propagate from a parent entity to its associated entities.

4.1. Cascade Types:

  1. ALL: Propagates all operations.
  2. PERSIST: Propagates save operations.
  3. MERGE: Propagates merge operations.
  4. REMOVE: Propagates delete operations.
  5. REFRESH: Propagates refresh operations.
  6. DETACH: Propagates detach operations.

Example of Cascade Types:

@Entity
public class Department {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @OneToMany(mappedBy = "department", cascade = CascadeType.ALL)
    private List<Employee> employees;
}
@Entity
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @ManyToOne
    @JoinColumn(name = "department_id")
    private Department department;
}

5. Mapping Combinations in Spring Boot

Spring Boot supports various combinations of entity mappings. Below are examples of each:

5.1. One-to-One Mapping:

@Entity
public class UserProfile {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @OneToOne
    @JoinColumn(name = "user_id")
    private User user;
}

5.2. One-to-Many Mapping:

@Entity
public class Department {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @OneToMany(mappedBy = "department")
    private List<Employee> employees;
}

5.3. Many-to-One Mapping:

@Entity
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @ManyToOne
    @JoinColumn(name = "department_id")
    private Department department;
}

5.4. Many-to-Many Mapping:

@Entity
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @ManyToMany
    @JoinTable(
        name = "student_course",
        joinColumns = @JoinColumn(name = "student_id"),
        inverseJoinColumns = @JoinColumn(name = "course_id")
    )
    private List<Course> courses;
}
@Entity
public class Course {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @ManyToMany(mappedBy = "courses")
    private List<Student> students;
}

6. Best Practices for Transactions

  1. Use class-level @Transactional for consistent transaction management across a service.
  2. Apply method-level @Transactional for fine-grained control over specific methods.
  3. Always set propagation and isolation explicitly to avoid unexpected behavior.
  4. Use CascadeType carefully to prevent unintentional operations.
  5. Log transaction behavior during development for easier debugging.

This document provides detailed insights into transactions, their isolation and propagation levels, cascade types, class vs method-level transactions, and mapping combinations in Spring Boot.

Leave a Comment