Spring Transaction Management


🔹 What is Transaction Management?


Transaction Management ensures that a set of database operations either:
  • All succeed → commit
  • All fail → rollback

It follows ACID properties:

  • Atomicity → All or nothing
  • Consistency → Database remains valid
  • Isolation → Transactions don’t interfere
  • Durability → Once committed, data is permanent
Example: Money Transfer
  1. Debit account A
  2. Credit account B

    👉 If step 2 fails, step 1 must roll back.



🔹 Transaction Management Approaches in Spring


1. Declarative Transaction Management
  • Uses @Transactional annotation (or XML configuration)
  • Recommended for most cases
  • Spring opens and closes transactions automatically
  • Clean, minimal boilerplate

a) Annotation-based (modern, recommended)


@Service

public class BankServiceDeclarative {

    private final AccountRepository repo;


    public BankServiceDeclarative(AccountRepository repo) {

        this.repo = repo;

    }


    @Transactional // Declarative, annotation-based

    public void transfer(Long fromId, Long toId, Double amount) {

        // --- Debit ---

        Account from = repo.findById(fromId).orElseThrow();

        from.setBalance(from.getBalance() - amount);

        repo.save(from);


        // --- Credit ---

        Account to = repo.findById(toId).orElseThrow();

        to.setBalance(to.getBalance() + amount);

        repo.save(to);

    }

}


b) XML-based (older, still supported)


public class BankServiceXml {

    private AccountRepository repo;


    public void setRepo(AccountRepository repo) {

        this.repo = repo;

    }


    public void transfer(Long fromId, Long toId, Double amount) {

        // Debit

        Account from = repo.findById(fromId).orElseThrow();

        from.setBalance(from.getBalance() - amount);

        repo.save(from);


        // Credit

        Account to = repo.findById(toId).orElseThrow();

        to.setBalance(to.getBalance() + amount);

        repo.save(to);

    }

}


spring-config.xml


<beans xmlns="http://www.springframework.org/schema/beans"

       xmlns:tx="http://www.springframework.org/schema/tx"

       xmlns:aop="http://www.springframework.org/schema/aop"

       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

       xsi:schemaLocation="

           http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd

           http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd

           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">


    <!-- Transaction Manager -->

    <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">

        <property name="entityManagerFactory" ref="entityManagerFactory"/>

    </bean>


    <!-- Transaction advice -->

    <tx:advice id="txAdvice" transaction-manager="transactionManager">

        <tx:attributes>

            <tx:method name="transfer" propagation="REQUIRED"/>

            <tx:method name="*" read-only="true"/>

        </tx:attributes>

    </tx:advice>


    <aop:config>

        <aop:pointcut id="bankServiceMethods" expression="execution(* com.example.service.BankServiceXml.*(..))"/>

        <aop:advisor advice-ref="txAdvice" pointcut-ref="bankServiceMethods"/>

    </aop:config>

</beans>


📌 Behavior:

  • Transaction opens at method entry
  • Commits on success
  • Rolls back on RuntimeException by default


2. Programmatic Transaction Management

  • Uses PlatformTransactionManager or TransactionTemplate
  • Explicitly starts/commits/rolls back transactions
  • Useful for fine-grained or multi-database logic
  • Verbose and more error-prone 

Example with PlatformTransactionManager


@Service

public class BankServiceProgrammatic {

    private final AccountRepository repo;

    private final PlatformTransactionManager txManager;


    public BankServiceProgrammatic(AccountRepository repo, PlatformTransactionManager txManager) {

        this.repo = repo;

        this.txManager = txManager;

    }


    public void transfer(Long fromId, Long toId, Double amount) {

        TransactionStatus status = txManager.getTransaction(new DefaultTransactionDefinition());

        try {

            // Debit

            Account from = repo.findById(fromId).orElseThrow();

            from.setBalance(from.getBalance() - amount);

            repo.save(from);


            // Credit

            Account to = repo.findById(toId).orElseThrow();

            to.setBalance(to.getBalance() + amount);

            repo.save(to);


            txManager.commit(status);

        } catch (Exception ex) {

            txManager.rollback(status);

            throw ex;

        }

    }

}



Example with TransactionTemplate


@Service

public class BankServiceTemplate {

    private final AccountRepository repo;

    private final TransactionTemplate txTemplate;


    public BankServiceTemplate(AccountRepository repo, TransactionTemplate txTemplate) {

        this.repo = repo;

        this.txTemplate = txTemplate;

    }


    public void transfer(Long fromId, Long toId, Double amount) {

        txTemplate.execute(status -> {

            Account from = repo.findById(fromId).orElseThrow();

            from.setBalance(from.getBalance() - amount);

            repo.save(from);


            Account to = repo.findById(toId).orElseThrow();

            to.setBalance(to.getBalance() + amount);

            repo.save(to);


            return null;

        });

    }

}



🔹 Comparison: Declarative vs Programmatic


Aspect

Declarative (@Transactional / XML)

Programmatic (TxManager / Template)

Definition

Annotations or XML rules

Manual code

Control

Spring manages

Full developer control

Boilerplate

Minimal

Verbose

Flexibility

Good (propagation, isolation)

Maximum (multi-DB, dynamic)

Use Cases

Service-level business logic

Complex flows, batch jobs

Error-prone

Less

More



🔹 Rollback Rules (with Examples)

  • Default: Rolls back for RuntimeException & Error
  • Does NOT rollback for checked exceptions (Exception, SQLException)

Example 1: Default rollback (RuntimeException 👍 )


@Transactional

public void transferRuntime(Long fromId, Long toId, double amount) {

    Account from = repo.findById(fromId).orElseThrow();

    from.setBalance(from.getBalance() - amount);

    repo.save(from);

    throw new RuntimeException("Simulated failure"); // rollback triggered

}

👉 Both debit & credit rollback.


Example 2: Checked exception does NOT rollback 👎


@Transactional

public void transferChecked(Long fromId, Long toId, double amount) throws Exception {

    Account from = repo.findById(fromId).orElseThrow();

    repo.save(from);

    throw new Exception("Checked exception"); // no rollback

}

👉 Debit is committed (partial update).


Example 3: Force rollback for checked exception 👍


@Transactional(rollbackFor = Exception.class)

public void transferRollbackForChecked(Long fromId, Long toId, double amount) throws Exception {

    Account from = repo.findById(fromId).orElseThrow();

    repo.save(from);

    throw new Exception("Will rollback now"); // rollback enforced

}


Example 4: Prevent rollback for RuntimeException 👍


public class NonCriticalException extends RuntimeException {}


@Transactional(noRollbackFor = NonCriticalException.class)

public void transferNoRollback(Long fromId, Long toId, double amount) {

    repo.save(new Account("A", 100.0));

    throw new NonCriticalException(); // still commits

}


Example 5: Catching exceptions inside @Transactional


@Transactional

public void transferCatch(Long fromId, Long toId, double amount) {

    try {

        throw new RuntimeException("Problem");

    } catch (RuntimeException e) {

        System.out.println("Caught inside, no rollback");

        // transaction commits

    }

}


If you want rollback while catching:


@Transactional

public void transferCatchAndRollback(Long fromId, Long toId, double amount) {

    try {

        throw new RuntimeException("Problem");

    } catch (RuntimeException e) {

        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

    }

}



🔹 Transaction Propagation Types with Examples


When one transactional method calls another, propagation defines transaction behavior.


Entities


@Entity

public class Account {

    @Id

    @GeneratedValue(strategy = GenerationType.IDENTITY)

    private Long id;

    private String owner;

    private Double balance;

    // getters/setters

}


Repository


public interface AccountRepository extends JpaRepository<Account, Long> {}


Service Layer


@Service

public class AccountService {

    private final AccountRepository repo;


    public AccountService(AccountRepository repo) {

        this.repo = repo;

    }


    @Transactional(propagation = Propagation.REQUIRED)

    public void deduct(Long accountId, Double amount) {

        Account acc = repo.findById(accountId).orElseThrow();

        acc.setBalance(acc.getBalance() - amount);

        repo.save(acc);

    }


    @Transactional(propagation = Propagation.REQUIRED)

    public void add(Long accountId, Double amount) {

        Account acc = repo.findById(accountId).orElseThrow();

        acc.setBalance(acc.getBalance() + amount);

        repo.save(acc);

    }

}



Bank Service – Propagation Examples

e.g. calling above methods deduct(), add() in BankService class


1. REQUIRED (default)

  • Joins existing transaction or creates new
  • If parent rolls back → child also rolls back

@Transactional(propagation = Propagation.REQUIRED)

public void transferRequired(Long fromId, Long toId, Double amount) {

    accountService.deduct(fromId, amount);   // joins parent tx

    if (amount > 1000) throw new RuntimeException("Failure");

    accountService.add(toId, amount);       // joins parent tx

}

👉 Behavior: If failure → both debit & credit rollback.



2. REQUIRES_NEW

  • Always starts a new transaction
  • Parent transaction is suspended

@Transactional(propagation = Propagation.REQUIRES_NEW)

public void transferRequiresNew(Long fromId, Long toId, Double amount) {

    accountService.deduct(fromId, amount);

    try {

        accountService.addRequiresNew(toId, amount); // runs independently

    } catch (Exception e) {

        // only child rollback, parent continues

    }

}

👉 Behavior: Debit may rollback, credit can commit independently.



3. MANDATORY

  • Must run inside existing transaction
  • Throws error if no parent transaction exists

@Transactional(propagation = Propagation.MANDATORY)

public void transferMandatory(Long fromId, Long toId, Double amount) {

    accountService.deduct(fromId, amount);

    accountService.addMandatory(toId, amount); // fails if no parent tx

}



4. SUPPORTS

  • Runs in transaction if parent exists, else non-transactional

@Transactional(propagation = Propagation.SUPPORTS)

public void transferSupports(Long fromId, Long toId, Double amount) {

    accountService.deduct(fromId, amount);

    accountService.addSupports(toId, amount); // joins or runs without tx

}



5. NOT_SUPPORTED

  • Always runs without transaction, suspends parent

@Transactional(propagation = Propagation.NOT_SUPPORTED)

public void transferNotSupported(Long fromId, Long toId, Double amount) {

    accountService.deduct(fromId, amount);

    accountService.addNotSupported(toId, amount); // runs without tx

}



6. NEVER

  • Fails if a transaction exists

@Transactional(propagation = Propagation.NEVER)

public void transferNever(Long fromId, Long toId, Double amount) {

    accountService.deduct(fromId, amount);

    accountService.addNever(toId, amount); // error if parent tx exists

}



7. NESTED

  • Creates a savepoint inside parent transaction
  • If child fails, only child rolls back

@Transactional(propagation = Propagation.NESTED)

public void transferNested(Long fromId, Long toId, Double amount) {

    accountService.deduct(fromId, amount);

    try {

        accountService.addNested(toId, amount); // rollback only nested

    } catch (Exception e) {

        // parent still continues

    }

}


Propagation Summary Table


Propagation

Behavior

Example Use Case

REQUIRED (default)

Join existing or create new

Money transfer (atomic debit + credit)

REQUIRES_NEW

Always new, suspends parent

Logging/Audit independent of main tx

MANDATORY

Must run inside existing tx

Child service forced into parent tx

SUPPORTS

Join if exists, else no tx

Read-only queries

NOT_SUPPORTED

Always run without tx

Sending email after transfer

NEVER

Fail if tx exists

Utility tasks, non-transactional operations

NESTED

Savepoint inside parent tx

Partially rolling back batch operations

 



🔹 Other @Transactional Attributes

1. readOnly
  • Purpose: Marks a transaction as read-only (no updates allowed).
  • Why: Optimizes performance — Spring can skip dirty-checking, Hibernate avoids unnecessary flushes.
  • Default: false (read/write).
  • Use case: Queries, reports, search operations.

@Transactional(readOnly = true)

public List<Account> getAllAccounts() {

    return repo.findAll(); // only reads, no modifications

}

👉 Benefit: Faster execution, protects against unintended writes.



2. isolation

  • Purpose: Defines how the transaction isolates itself from other concurrent transactions.
  • Default: Isolation.DEFAULT (uses DB’s default, usually READ_COMMITTED).
  • Options:
    • READ_UNCOMMITTED → Allows dirty reads (rarely used).
    • READ_COMMITTED → Prevents dirty reads (common default).
    • REPEATABLE_READ → Prevents non-repeatable reads.
    • SERIALIZABLE → Full isolation, prevents phantom reads (slowest, safest).

@Transactional(isolation = Isolation.SERIALIZABLE)

public void transferSafe(Long fromId, Long toId, Double amount) {

    // Highest isolation for critical transactions

    Account from = repo.findById(fromId).orElseThrow();

    from.setBalance(from.getBalance() - amount);

    repo.save(from);


    Account to = repo.findById(toId).orElseThrow();

    to.setBalance(to.getBalance() + amount);

    repo.save(to);

}

👉 Tradeoff: Higher isolation = safer, but more locking & lower performance.



3. rollbackFor

  • Purpose: Rollback for specified exceptions (checked or unchecked).
  • Default: Rolls back on RuntimeException & Error only.

@Transactional(rollbackFor = SQLException.class)

public void updateWithSqlException(Long accountId) throws SQLException {

    Account acc = repo.findById(accountId).orElseThrow();

    acc.setBalance(acc.getBalance() + 500);

    repo.save(acc);

    throw new SQLException("Simulated DB error"); // forces rollback

}

👉 Ensures rollback even for checked exceptions.



4. noRollbackFor

  • Purpose: Prevent rollback for specific exceptions.
  • Default: None.

public class BusinessWarningException extends RuntimeException {}


@Transactional(noRollbackFor = BusinessWarningException.class)

public void updateNoRollback(Long accountId) {

    Account acc = repo.findById(accountId).orElseThrow();

    acc.setBalance(acc.getBalance() + 200);

    repo.save(acc);

    throw new BusinessWarningException(); // will NOT rollback

}

👉 Useful for non-critical errors where partial commit is okay.



5. timeout

  • Purpose: Sets max time (in seconds) a transaction can run before auto-rollback.
  • Default: No limit (depends on DB).
  • Use case: Long-running jobs where you want to fail fast if stuck.

@Transactional(timeout = 5) // must complete within 5 seconds

public void processLargeTransaction(Long accountId) {

    // some heavy operation

}

👉 Prevents DB connection starvation.



6. value / transactionManager

  • Purpose: Specify the transaction manager bean (when multiple are configured).
  • Default: Uses primary PlatformTransactionManager.
  • Use case: Multi-database or JMS + DB transactions.

@Transactional("secondaryTransactionManager")

public void processWithSecondaryDB(Long accountId) {

    // Uses another DB transaction manager

}



🔹 Summary tables


1. Quick Reference Table for @Transactional Attributes


Attribute

Description

Default

Example

propagation

Defines transaction participation behavior

REQUIRED

@Transactional(propagation = Propagation.REQUIRES_NEW)

isolation

Sets concurrency isolation level

DEFAULT (DB config)

@Transactional(isolation = Isolation.SERIALIZABLE)

readOnly

Optimizes for read-only operations

false

@Transactional(readOnly = true)

rollbackFor

Forces rollback for given exceptions

RuntimeException, Error

@Transactional(rollbackFor = SQLException.class)

noRollbackFor

Prevents rollback for given exceptions

None

@Transactional(noRollbackFor = BusinessWarningException.class)

timeout

Maximum execution time (seconds)

None

@Transactional(timeout = 30)

value / transactionManager

Selects which Tx manager to use

Primary bean

@Transactional("secondaryTxManager")

This table is super useful for quickly choosing attributes when writing transactional methods.



2. Rollback Behavior Summary


Exception Type

Default Behavior

Can Configure

RuntimeException

Rollback

noRollbackFor to prevent rollback

Checked Exception

No rollback

rollbackFor to enforce rollback

Error

Rollback

Cannot change



3. Isolation Levels (Optional Advanced Section)


Isolation Level

Description

Use Case

DEFAULT

Uses DB default

Most cases

READ_UNCOMMITTED

Dirty reads allowed

Very rare, high performance

READ_COMMITTED

Prevents dirty reads

Common default in many DBs

REPEATABLE_READ

Prevents non-repeatable reads

Medium isolation

SERIALIZABLE

Highest isolation, prevents phantom reads

Critical financial transactions



4. Transactional Method Checklist

Before marking a method @Transactional, check:

  • Is this method modifying data? Use transactional
  • Is this method read-only? Use readOnly = true
  • Does it call other transactional methods? Decide propagation
  • Do you need custom rollback rules? Set rollbackFor / noRollbackFor
  • Is it long-running? Consider timeout
  • Will it interact with multiple databases? Programmatic management may be better


5. Quick Propagation Decision Table 


Scenario

Recommended Propagation

Simple service-level method

REQUIRED

Independent audit/log

REQUIRES_NEW

Must run inside existing tx

MANDATORY

Optional transactional method

SUPPORTS

Should never run in tx

NEVER

Long-running method that can suspend tx

NOT_SUPPORTED

Partial rollback within parent tx

NESTED





Best Practices & Key Considerations in 
Spring Transaction Management

  1. Use @Transactional Correctly

    • Apply only on public methods (Spring AOP proxies don’t intercept private/protected).
    • Avoid self-invocation (a method calling another method in the same class won’t trigger transactional proxy).
    • For internal calls, refactor into another bean or enable AspectJ mode.
    @Service public class UserService { @Transactional public void saveUser(User user) { repo.save(user); } // ❌ Won’t start a transaction if called internally @Transactional private void helper(User user) { repo.save(user); } }

  1. Keep Transactions Short

    • Do not include long-running operations (e.g., file I/O, external API calls).
    • Open transactions only around DB work, close them quickly.
    • Prevents deadlocks and reduces lock contention.

  1. Use Read-Only Transactions for Queries

    • @Transactional(readOnly = true) improves performance.
    • Hibernate skips dirty-checking and avoids unnecessary flushes.
    • Good for search, reporting, read-only services.

  1. Choose Propagation Carefully

    • Use REQUIRED (default) for most service-level logic.
    • Use REQUIRES_NEW for independent operations (e.g., audit logging).
    • Use NESTED for partial rollbacks in batch processing.
    • Avoid MANDATORY/NEVER unless explicitly needed.

  1. Select Isolation Level Based on Use Case

    • READ_COMMITTED (safe default).
    • REPEATABLE_READ for consistency in multiple reads.
    • SERIALIZABLE for critical financial transactions (but slower).
    • Tradeoff: Higher isolation = safer but lower concurrency.

  1. Rollback Rules

    • By default → Rollback on RuntimeException & Error.
    • Use rollbackFor to rollback on checked exceptions.
    • Use noRollbackFor to commit despite exceptions.
    • Explicitly call setRollbackOnly() when catching exceptions inside transactions.

  1. Multiple Transaction Managers

    • Use @Transactional("txManagerName") if multiple managers exist.
    • Example: Separate DB + JMS transactions.
    • In microservices → consider Saga / Outbox patterns instead of XA (distributed transactions).

  1. Testing Transactions

    • Spring rolls back automatically for test methods annotated with @Transactional.
    • Use @Commit to persist data in test DB.
    • Use @Rollback(false) to disable rollback for specific tests.
    @SpringBootTest @Transactional // auto rollback class BankServiceTest { @Test void testTransfer() { ... } }

  1. Lazy Loading & Transactions

    • Accessing lazy-loaded entities outside of a transaction causes LazyInitializationException.

    • Fixes:

      • Use Open Session in View (OSIV) carefully.
      • Or fetch required associations eagerly in the service layer.

  1. Transaction Boundaries in Microservices

  • A single transaction cannot span multiple services.
  • Avoid XA / 2PC in microservices (complex, slow).
  • Prefer Saga pattern (orchestration/choreography) for long-running business processes.

These practices ensures safe, performant, and predictable transaction handling in Spring applications.