Managing Transactions with Spring Framework

In database operations, a transaction is treated as a single unit of work. To ensure data integrity, transactions adhere to four key principles, often abbreviated as ACID:

  • Atomicity: Ensures that all operations within the transaction succeed. If any step fails, the entire transaction is aborted, and no changes are applied.
  • Consistency: Maintains the database in a valid state before and after the transaction. For example, in a fund transfer, the total amount of money remains constant; money is neither lost nor created.
  • Isolation: Ensures that concurrent transactions do not interfere with one another. The result of concurrent transactions should be the same as if they were executed sequentially.
  • Durability: Guarantees that once a transaction is committed successfully, the changes persist in the database even in the event of a system failure.

Implementing a Fund Transfer Scenario

To illustrate transaction management, we will implement a banking use case where one user transfers funds to another. We will begin by setting up the environment without transactions to highlight potential data inconsistencies.

1. Database Setup

Create a table to store account information and insert initial records.

CREATE TABLE wallet_table (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_name VARCHAR(50) NULL,
    balance INT NULL
);

-- Initial data setup
INSERT INTO wallet_table (user_name, balance) VALUES ('Alice', 1000);
INSERT INTO wallet_table (user_name, balance) VALUES ('Bob', 1000);

2. Project Configuration and Dependencies

We need to configure the data source and JdbcTemplate in the Spring configuration file. The structure involves a Service layer handling business logic and a DAO layer handling database access.

XML Configuration (spring-config.xml):

<context:component-scan base-package="com.example.demo"/>

<bean id="sourceData" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="url" value="jdbc:mysql://localhost:3306/bank_db"/>
    <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
    <property name="username" value="root"/>
    <property name="password" value="password"/>
</bean>

<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <property name="dataSource" ref="sourceData"/>
</bean>

3. DAO Layer Implementation

The DAO layer provides methods to credit and debit accounts.

Interface:

package com.example.demo.dao;

public interface BankDao {
    void increaseBalance(String name, int amount);
    void decreaseBalance(String name, int amount);
}

Implementation:

package com.example.demo.dao;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

@Repository
public class BankDaoImpl implements BankDao {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public void increaseBalance(String name, int amount) {
        String sql = "UPDATE wallet_table SET balance = balance + ? WHERE user_name = ?";
        jdbcTemplate.update(sql, amount, name);
    }

    @Override
    public void decreaseBalance(String name, int amount) {
        String sql = "UPDATE wallet_table SET balance = balance - ? WHERE user_name = ?";
        jdbcTemplate.update(sql, amount, name);
    }
}

4. Service Layer Implementation

The Service layer orchestrates the transfer by calling the DAO methods.

package com.example.demo.service;

import com.example.demo.dao.BankDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class TransferService {

    @Autowired
    private BankDao bankDao;

    public void executeTransfer() {
        // Alice sends 100
        bankDao.decreaseBalance("Alice", 100);

        // Simulating a network failure or system crash
        int result = 50 / 0; 

        // Bob receives 100
        bankDao.increaseBalance("Bob", 100);
    }
}

5. Testing the Scenario

package com.example.demo.test;

import com.example.demo.service.TransferService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class AppTest {
    @Test
    public void testTransfer() {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");
        TransferService service = context.getBean(TransferService.class);
        service.executeTransfer();
    }
}

6. Analyzing the Failure

When the test runs, the arithmetic exception occurs after Alice's balance is deducted but before Bob's balance is updated. As a result, the database shows an inconsistent state: Alice has 900, but Bob still has 1000. The 100 units are effectively lost.

Resolving Inconsistency with Spring Transactions

To fix this, we must ensure that the database operations are atomic. The standard approach involves: 1. Opening a transaction. 2. Executing business logic. 3. Committing the transaction if successful. 4. Rolling back the transaction if an exception occurs.

Spring simplifies this through Declarative Transaction Management, which typically uses AOP (Aspect-Oriented Programming).

Step 1: Configure Transaction Manager

We must define a transaction manager bean and enable annotation-driven transaction management in the XML configuration.

<!-- Transaction Manager Configuration -->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="sourceData"/>
</bean>

<!-- Enable Transaction Annotations -->
<tx:annotation-driven transaction-manager="txManager"/>

Step 2: Apply @Transactional Annotation

Add the @Transactional annotation to the Service class or specific methods. This instructs Spring to manage the transaction boundary automatically.

import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
public class TransferService {
    // ... dependencies and methods remain the same

    public void executeTransfer() {
        bankDao.decreaseBalance("Alice", 100);
        
        // Simulating failure
        int result = 50 / 0; 
        
        bankDao.increaseBalance("Bob", 100);
    }
}

Now, reset the balances to 1000 for both users and run the test again. Although the exception is still thrown, Spring intercepts the exception and triggers a rollback. Both accounts will retain their original 1000 balance, ensuring data consistency.

@Transactional Configuration Parameters

The @Transactional annotation supports several parameters to fine-tune transaction behavior:

@Transactional(
    propagation = Propagation.REQUIRED, 
    isolation = Isolation.REPEATABLE_READ, 
    timeout = 30, 
    readOnly = false
)

1. Propagation (Transaction Propagation Behavior)

Defines how transacsions relate to each other when multiple transactional methods call one another.

  • REQUIRED: Default. Joins the existing transaction if one exists; creates a new one otherwise.
  • REQUIRES_NEW: Always creates a new transaction, suspending any existing transaction.
  • SUPPORTS: Runs within the existing transaction if available; executes non-transactionally otherwise.
  • NOT_SUPPORTED: Executes non-transactionally, suspending any existing transaction.
  • MANDATORY: Must run within an existing transaction; throws an exception if none exists.
  • NEVER: Must not run within a transaction; throws a exception if a transaction exists.
  • NESTED: Runs within a nested transaction if a transaction exists; creates a new one otherwise.

2. Isolation (Transaction Isolation Levels)

Isolation levels prevent issues caused by concurrent transactions, such as dirty reads, non-repeatable reads, and phantom reads.

  • Dirty Read: Reading data written by an uncommitted transaction.
  • Non-Repeatable Read: Reading different data when a row is modified and committed by another transaction between two reads.
  • Phantom Read: Seeing new rows when a transaction adds data and commits between two reads of the same range.

Isolation Levels Matrix:

Isolation Level Dirty Read Non-Repeatable Read Phantom Read
READ UNCOMMITTED Yes Yes Yes
READ COMMITTED No Yes Yes
REPEATABLE READ No No Yes
SERIALIZABLE No No No

3. Timeout

Specifies the time in seconds the transaction is allowed to run before being rolled back automatically. The default is -1 (no timeout).

4. ReadOnly

Optimizes the transaction for read-only operations. Set to true for queries to prevent potential dirty writes. Default is false.

5. Rollback Rules

  • rollbackFor: Defines an array of exception classes that should trigger a rollback. By default, unchecked exceptions (RuntimeException) trigger rollback.
  • noRollbackFor: Defines an array of exception classes that should not trigger a rollback.

Tags: Spring Framework Transaction Management java ACID database

Posted on Tue, 07 Jul 2026 16:47:18 +0000 by Benny007