DEV Community

Abdulfatai Abdulwasiu Aremu
Abdulfatai Abdulwasiu Aremu

Posted on

⚙️ Mastering Transactions in Java Spring Boot: A Developer's Guide

Imagine this: you're transferring money from one bank account to another. The amount gets debited from your account, but due to a power outage or system crash, it never reaches the recipient. Now both parties are at a loss.
That's exactly the kind of problem transactions solve. In this guide, we'll dive deep into what transactions are, how Spring Boot makes them easy to implement, and walk through a practical bank transfer example to see them in action.

🧠 Understanding Transactions
A transaction in software terms is a sequence of operations performed as a single logical unit of work. The operations must either all succeed or all fail together — no middle ground. This approach ensures the integrity and consistency of data, especially in mission-critical systems like banking, inventory management, or order processing.

To enforce this, transactions are built around the ACID principles:

  • *Atomicity * ensures that either all steps in a transaction happen or none do.
  • Consistency guarantees that the database stays in a valid state before and after the transaction.
  • Isolation keeps transactions from interfering with each other while executing simultaneously.
  • Durability ensures that once a transaction is completed, the changes are permanently saved—even if the system crashes right after.

🚀 Transaction Management in Spring Boot
Spring Boot uses a simple and powerful annotation called @Transactional to wrap a method in a transaction. Once you annotate a service method, Spring handles all the behind-the-scenes magic like opening a transaction, committing it if successful, or rolling it back in case of failure.
But how does this work practically? Let’s go through an example step by step.

🏦 Building a Bank Transfer Example
Let’s assume we’re building a small banking system. A user should be able to transfer money from one account to another. If anything goes wrong during the process, we want the whole transaction to roll back.

Step 1: Define the Account Entity

@Entity
public class Account {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String owner;
    private Double balance;

    // Getters and setters
}
Enter fullscreen mode Exit fullscreen mode

This is a simple JPA entity that represents a bank account with an owner and a balance.
Step 2: Create the Repository

public interface AccountRepository extends JpaRepository<Account, Long> {
}
Enter fullscreen mode Exit fullscreen mode

The repository gives us CRUD access to the Account data.
Step 3: Write the Service Layer with Transactions

@Service
public class BankService {

    @Autowired
    private AccountRepository repository;

    @Transactional
    public void transfer(Long fromAccountId, Long toAccountId, Double amount) {
        Account sender = repository.findById(fromAccountId)
                .orElseThrow(() -> new RuntimeException("Sender account not found"));

        Account receiver = repository.findById(toAccountId)
                .orElseThrow(() -> new RuntimeException("Receiver account not found"));

        if (sender.getBalance() < amount) {
            throw new RuntimeException("Insufficient funds");
        }

        sender.setBalance(sender.getBalance() - amount);
        receiver.setBalance(receiver.getBalance() + amount);

        repository.save(sender);
        repository.save(receiver);

        // Simulate an error to trigger rollback
        // throw new RuntimeException("Simulated transfer failure");
    }
}
Enter fullscreen mode Exit fullscreen mode

Here’s the heart of it: the @Transactional annotation ensures that if any exception occurs during the transfer (even after the first save), Spring will automatically roll back all database changes. This prevents half-completed operations that could lead to corrupted data.
Step 4: Expose the Transfer Endpoint

@RestController
public class BankController {

    @Autowired
    private BankService bankService;

    @PostMapping("/transfer")
    public ResponseEntity<String> transfer(@RequestParam Long from,
                                           @RequestParam Long to,
                                           @RequestParam Double amount) {
        bankService.transfer(from, to, amount);
        return ResponseEntity.ok("Transfer successful");
    }
}
Enter fullscreen mode Exit fullscreen mode

When a user calls this endpoint, money is transferred from one account to another — and if something fails, it fails safely.

🔁 What Happens on Failure?
If the service method throws a RuntimeException (or an unchecked exception), Spring catches it and triggers a rollback. That means any balance changes, even if saved, are undone before being committed to the database.
If you want to roll back on checked exceptions (like IOException), you’ll need to explicitly specify:

@Transactional(rollbackFor = IOException.class)
Enter fullscreen mode Exit fullscreen mode

This tells Spring to treat those exceptions as fatal and rollback-worthy too.

🛡️ Best Practices for Transactions

  • Always place @Transactional on service layer methods, not controllers or repositories.
  • Avoid complex logic inside transactional methods; keep them focused and minimal.
  • Be careful with lazy-loaded entities — once the transaction closes, those proxies can’t fetch additional data.
  • Use logging or monitoring to track transaction boundaries during debugging.

🎨 Visual Flow: Transaction in Action

Client ➜ /transfer
        ↓
BankController ➜ Calls BankService.transfer()
        ↓
@Transactional boundary starts
        ↓
1. Debit sender
2. Credit receiver
3. Save both accounts
        ↓
Error? ➜ Rollback changes
Success? ➜ Commit transaction
Enter fullscreen mode Exit fullscreen mode

🧪 How to Test Transactions
Testing transaction rollbacks is just as important as implementing them.

@SpringBootTest
public class BankServiceTest {

    @Autowired
    private BankService bankService;

    @Test
    void testTransactionRollback() {
        // Create mock accounts with balances
        // Attempt to transfer with forced exception
        // Assert balances are unchanged
    }
}
Enter fullscreen mode Exit fullscreen mode

You can simulate a failure during a transfer and ensure balances are reverted. This confirms that transactions are working correctly.

🎯 Final Thoughts
With just a few annotations and the right architecture, Spring Boot makes transaction management seamless. Whether you're building a simple app or a distributed enterprise system, transactions help ensure your data stays reliable and consistent.

By understanding how @Transactional works and how to structure your service layers properly, you can build robust applications that fail gracefully.

Top comments (0)