Featured resource
2025 Tech Upskilling Playbook
Tech Upskilling Playbook

Build future-ready tech teams and hit key business milestones with seven proven plays from industry leaders.

Check it out
  • Lab
    • Libraries: If you want this lab, consider one of these libraries.
    • Core Tech
Labs

Guided: Spring Certified Professional - Managing Transactions with Spring

Transaction management ensures data integrity and consistency in applications. In this guided lab, you’ll implement Spring’s transaction management features including propagation, rollback rules, and test integration. Ideal for backend developers preparing for Spring certification, this lab gives you hands-on experience to control data consistency with confidence.

Lab platform
Lab Info
Level
Intermediate
Last updated
Oct 13, 2025
Duration
1h 1m

Contact sales

By clicking submit, you agree to our Privacy Policy and Terms of Use.
Table of Contents
  1. Challenge

    Introduction

    In this lab, you will learn how to manage transactions in Spring Boot using a banking application. You will use declarative transaction management, marking methods with annotations to ensure that a series of database operations occur as a single atomic unit (all succeed or all fail). The use case is a simple fund transfer between bank accounts. The application uses an mysql database to simulate accounts and transaction records.


    Learning Objectives:

    • Describe how Spring’s declarative transaction management works.
    • Enable and configure Spring Transaction Management in a Spring Boot app.
    • Manage transaction propagation behaviors for nested transactions.
    • Define rollback rules for specific exceptions.
    • Utilize transactions in tests to ensure data consistency between test methods.

    Use Case Scenario:

    You are building a banking service that allows customers to transfer money between accounts. When a transfer is initiated, the system should:

    • Debit (withdraw) the amount from the source account.
    • Credit (deposit) the amount to the destination account.
    • Record the transfer details in a Transaction table.

    These operations must be wrapped in a single transaction, if any step fails (for example, an error occurs or a validation triggers insufficient funds), all changes should roll back so no account is left with partial updates. Spring’s transaction management will ensure data integrity in these scenarios. info>If you get stuck on a task, you can view the solution in the solution folder in your file tree, or click the Task Solution link at the bottom of each task after you've attempted it.

  2. Challenge

    Enable Transaction Management

    What is a transaction?

    A transaction is a unit of work that must be completed entirely or not at all. It ensures atomicity, consistency, isolation, and durability (ACID).

    • Atomicity : All operations within a transaction are either completely successful or none are.
    • Consistency : Data remains in a consistent state before and after a transaction.
    • Isolation : Transactions do not interfere with each other.
    • Durability : Once a transaction is committed, changes are persisted.

    In financial applications like banking, this guarantees that a transfer either fully completes (debit and credit) or not at all.


    The lab uses a Spring Boot application connected to a MySQL database named banking. The schema includes an account table preloaded with sample data. You will be working with a preconfigured service layer (AccountService), repository layer (AccountRepository), and a REST controller (AccountController).

    This setup ensures that you can focus purely on modifying business logic to explore transactional behaviors using @Transactional, propagation types, rollback rules, and test transactions.

    --- Spring provides an abstraction layer over transaction APIs (like JDBC, JPA, and JTA) through the @Transactional annotation. This lets developers manage transactions declaratively without boilerplate code.

    In this step, you will enable Spring’s annotation-driven transaction management in the application. Spring Boot automatically includes Spring’s transaction support when spring-tx and relevant dependencies are on the classpath, but it’s good practice (and sometimes necessary) to explicitly enable transaction management in your configuration.

    @SpringBootApplication  
    @EnableTransactionManagement  
    public class BankApplication {  
        // ...  
    }
    

    This single @EnableTransactionManagement annotation tells Spring to activate its transaction management infrastructure. It will scan for methods annotated with @Transactional and wrap them with a proxy that handles starting and committing or rolling back transactions automatically.

    You will now observe the behavior of the system during a fund transfer operation. This will help you understand how data consistency is affected when transactions are not explicitly managed.

    Begin by reviewing the transferFunds method in the AccountService class to understand its current implementation.


    To inspect the current state of the database, you can run the following query:

    select * from accounts;
    

    Steps to run query:

    1. Go to the query.sql file located in the top right section.
    2. Open the SQL Viewer in the bottom panel.
    3. Choose banking as the database and click on Run button at the bottom right of screen.

    This allows you to execute SQL queries directly against the database.

    In order to reset the database contents you can similarly run the db.sql.


    As you can see, the current contents of the accounts table are as follows:

    Table : accounts

    | ID | Account Holder | Balance | | -- | -------------- | ------- | | 1 | Alice Johnson | 1000 | | 2 | Bob Smith | 2000 | | 3 | Charlie Brown | 3000 | | 4 | David Parker | 4000 | | 5 | Elizabeth Swan | 5000 | Open the file AccountServiceTest located in directory banking/src/test/com/globomantics/banking/service/

    You will now test two scenarios:

    First, perform a transfer using a valid destination account, which should complete successfully. Then, perform another transfer using an invalid destination account, which should fail.

    In both cases, the system is expected to maintain consistent data. This means the entire operation should either be applied fully or not at all, without leaving any partial changes in the database.

    In the Terminal, navigate to the banking folder.

    cd banking
    ``` After the transfer you can see query the database to see the output as below. 
    
    >#### Table : accounts
    
    | ID | Account Holder | Balance |
    | -- | -------------- | ------- |
    | 1  | Alice Johnson  |  <span style="color:green">900</span>   (Debit by 100) |
    | 2  | Bob Smith      | <span style="color:green">2100</span>    (Credit by 100)|
    | 3  | Charlie Brown  | 3000    |
     info> Each test will reset the `accounts` table to its initial state before execution. The account balances will be set to 1000, 2000, and 3000 respectively. This ensures that test results remain consistent every time the tests are run.
    
    Next you will see what happens when you try to send money to an account number which does not exist.
    
     The test case **Fails** as it expects both the debit and credit transaction to not take place if the destination account number is incorrect. 
    
    As you can see this has resulted in inconsistent database state.
    
    >#### Table : accounts
    
    | ID | Account Holder  | Balance                  |
    |----|------------------|--------------------------|
    | 1  | Alice Johnson    | 1000 |
    | 2  | Bob Smith        | 2000                     |
    | 3  | Charlie Brown    | <span style="color:red">2800</span> (Debit by 200)                     |
    | 4  | David Parker       | 4000                     |
    | 5  | Elizabeth Swan       | 5000                     | Now you will apply `@Transactional` annotation over the `transferFunds` method so that incase of any `RuntimeException` the entire transaction is rolled back.  Now, retry the same test.  After adding the `@Transactional` annotation, the transaction is now properly managed. When a failure occurs during the transfer, no partial updates are committed. The account balances remain unchanged, reflecting a consistent state in the database. This confirms that the transaction is rolled back correctly and the operation behaves as expected.
    
    >#### Table : accounts
    | ID | Account Holder  | Balance                  |
    |----|------------------|--------------------------|
    | 1  | Alice Johnson    | 1000 |
    | 2  | Bob Smith        | 2000                     |
    | 3  | Charlie Brown    | <span style="color:green">3000</span> (No Change)|
    | 4  | David Parker  | 4000    |
    | 5  | Elizabeth Swan  | 5000    |
     In this step, you learned how to use `@Transactional` to wrap service methods in a transaction and ensure atomic operations.
    In the next step, you'll explore how to coordinate transactions across multiple methods using propagation settings.
  3. Challenge

    Configuring Transaction Propagation

    Now that you've understood basic transaction boundaries using the @Transactional annotation, it's time to see how Spring handles transactions when one transactional method calls another. This is where transaction propagation comes into play.

    In this step, you'll explore how to manage independent or shared transaction scopes using propagation types like REQUIRED and REQUIRES_NEW. With practical examples, you'll understand how these settings affect commit and rollback behavior when combining service methods.

    Understanding Propagation Types in Spring

    Propagation defines how a method should behave when called inside an existing transaction. This is especially important in layered architectures where one service method calls another. Spring provides several propagation behaviors, each with distinct transactional behavior.


    REQUIRED

    This is the default propagation type. The method will join the existing transaction if one exists; otherwise, a new transaction is started.

    Example:

    @Transactional(propagation = Propagation.REQUIRED)
    public void updateAccountBalance(int id, double amount) {
        // joins existing transaction if present
        // otherwise, starts a new one
    }
    

    REQUIRES_NEW

    Always creates a new transaction, suspending any existing transaction. Useful for logging or auditing operations that should persist even if the outer transaction fails.

    Example:

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void logAuditEntry(String message) {
        // runs in its own transaction
    }
    

    NESTED

    Starts a nested transaction within the existing transaction. If the nested transaction fails, only the nested part rolls back using a savepoint, while the outer transaction can still commit.

    Example:

    @Transactional(propagation = Propagation.NESTED)
    public void saveDraftTransaction(Transaction tx) {
        // rollback only the nested part if needed
    }
    

    MANDATORY

    Requires an existing transaction. If none exists, an exception is thrown.

    Example:

    @Transactional(propagation = Propagation.MANDATORY)
    public void validateSession() {
        // must be called within a transaction
    }
    

    NEVER

    Must not run inside a transaction. If a transaction is active, an exception is thrown.

    Example:

    @Transactional(propagation = Propagation.NEVER)
    public void sendExternalNotification() {
        // should not be transactional
    }
    

    NOT_SUPPORTED

    Suspends the current transaction (if any) and runs non-transactionally.

    Example:

    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void logNonCriticalEvent() {
        // executes outside any transaction
    }
    

    SUPPORTS

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

    Example:

    @Transactional(propagation = Propagation.SUPPORTS)
    public void readOnlyOperation() {
        // joins if transaction exists
    }
    

    Choosing the right propagation type allows fine-grained control over how your code handles commit and rollback scenarios. It ensures consistency, especially when multiple methods interact with the database within the same request lifecycle. Proper configuration helps prevent unintended commits or rollbacks. info> If you do not specify the propagation type (i.e., just use @Transactional), Spring uses Propagation.REQUIRED by default.


    In the application, there is an audit functionality that logs every fund transfer operation by recording the source account, destination account, amount, and timestamp in the account_audit table. Ideally, you want audit records to be persisted regardless of whether the fund transfer succeeds or fails.

    Now you will run the success and failure test cases together and observe the entries in the account_audit table. info>Reset the database by running the db.sql file in SQL Browser.

    Run the following commands:

    Transfer funds from Account 1 to Account 2

    mvn -Dtest=AccountServiceTest#testSuccessfulTransfer test
    

    Transfer funds from Account 3 to Account 400

    mvn -Dtest=AccountServiceTest#testTransferToIncorrectAccount test
    

    You expect the auditTransfer method to capture both of these transactions in the account_audit table.

    Write the below sql in query.sql and Run it from SQL Viewer.

    use banking; 
    
    select * from account_audit;
    ``` The `account_audit` table has only audit entries for successful transfers, missing the transfers where account number was wrong.
    
    >#### Table : account_audit
    | Audit ID | From Account | To Account | Amount| Timestamp |
    | -- | -------------- | ------- | ----| --| 
    | 1  | 1  | 2    | 100|2025-07-14 01:54:43| Currently when the transfer fails, the entire transaction including the audit log is rolled back.
    
    However, you want the audit records to be preserved regardless of whether the transfer succeeds.
    
    To achieve this, you'll now set the `auditTransfer` method’s propagation to `REQUIRES_NEW`.
    This change ensures that the audit operation runs in a separate transaction and is committed independently, even if the outer transfer transaction fails. ---
    Rerun the test case so that the audit entries are captured. 
    
    Transfer from Account **3** to account **400**.
    
    ```java
    mvn -Dtest=AccountServiceTest#testTransferToIncorrectAccount test
    

    When you query the account_audit table, you will notice that audit records are present even for failed transfers.

    Table : account_audit

    | Audit ID | From Account | To Account | Amount| Timestamp | | -- | -------------- | ------- | ----| --| | 1 | 1 | 2 | 100|2025-07-14 01:54:43| | 2 | 3 | 400 | 200|2025-07-14 01:58:54 | In this step, you learned how to use different propagation types to control transaction boundaries across service methods. In the next step, you'll configure rollback behavior for both checked and unchecked exceptions.

  4. Challenge

    Setting Up Rollback Rules

    Now that you've worked with transaction propagation, your next focus is understanding how exceptions affect transactions.

    By default, Spring rolls back transactions only for unchecked (runtime) exceptions. If your method throws a checked exception, the transaction will not roll back unless explicitly configured.

    In this step, you'll learn how to override this behavior using the rollbackFor attribute in @Transactional. You'll then apply this knowledge by modifying your service to roll back for a custom checked exception like BlackListedAccountException. ---

    In this task, you will introduce a custom checked exception named BlackListedAccountException to simulate a business rule violation, specifically when a fund transfer is attempted to or from a blacklisted account.

    This exception extends Exception (not RuntimeException), which means Spring will not automatically roll back the transaction unless explicitly configured. This task helps demonstrate how Spring behaves with checked exceptions during transactional operations and why configuring rollback rules using rollbackFor is important to maintain data consistency.

    Now you will test a transfer to a blacklisted account. As you can see, even though the test case failed, Spring did not roll back the transaction because the exception was a checked exception.

    select * from accounts;
    

    Table : accounts

    | ID | Account Holder | Balance | | -- | -------------- | ------- | | 1 | Alice Johnson | 1000 | | 2 | Bob Smith | 2000 | | 3 | Charlie Brown | 3000 | | 4 | David Parker | 2500 (Debit by 1500) | | 5 | Elizabeth Swan | 5000 | Next you will explicitly configure Spring to roll back the transaction when the InsufficientFundsException is thrown. You will do this by adding the rollbackFor attribute to the @Transactional annotation on the transferFunds method.

    This will ensure that no partial changes are committed when a business rule like insufficient funds causes a checked exception to be thrown. You will observe that neither the debit nor credit operations are saved to the database in this scenario. Now you will rerun the test. It should pass. In this step, you learned how Spring handles rollback behavior and how to explicitly configure it using rollbackFor to ensure data integrity with checked exceptions. In the next step, you'll validate transactional behavior using automated test cases.

  5. Challenge

    Using Transactions in Tests

    Writing transactional code alone is not sufficient. You also need to verify that it behaves as expected during testing. Spring provides built-in support for managing transactions in test methods.

    In this step, you will create tests that are automatically wrapped in transactions. This ensures any changes made during the test are rolled back afterward. As a result, your database stays clean and tests remain isolated and repeatable.

    Spring's test framework integrates with its transaction manager. Annotating a test method with @Transactional ensures that:

    • A transaction is started before the test method.

    • All changes are automatically rolled back after the method completes.

    This keeps tests isolated, repeatable, and free from data side effects.


    info>Reset the database by running the db.sql file in SQL Browser. As you've previously seen, after running the test the values in the database are updated as below.

    select * from accounts;
    

    Table : accounts

    | ID | Account Holder | Balance | | -- | -------------- | ------- | | 1 | Alice Johnson | 900 (Debit by 100) | | 2 | Bob Smith | 2100 (Credit by 100)| | 3 | Charlie Brown | 3000 | | 4 | David Parker | 4000 | | 5 | Elizabeth Swan | 5000 | Ideally, the Test should automatically roll back any changes it makes. To enable this behavior, you will annotate the test method with @Transactional. info>Reset the database contents by running db.sql and rerun the same test.

    select * from accounts;
    

    Table : accounts

    | ID | Account Holder | Balance | | -- | -------------- | ------- | | 1 | Alice Johnson | 1000 (No Change) | | 2 | Bob Smith | 2000 (No Change)| | 3 | Charlie Brown | 3000 | | 4 | David Parker | 4000 |

    As you can observe, the test case continues to pass, but no changes are applied to the database. This guarantees that the test remains repeatable and produces consistent results each time it runs.

    --- In this step, you learned how to isolate database changes within test methods using @Transactional, ensuring consistent test results. In the next step, you will review everything you’ve implemented and explore more advanced transaction management topics.

  6. Challenge

    Conclusion and Next Steps

    You've now completed a full walkthrough of managing transactions using Spring Boot. Throughout this lab, you explored declarative transaction management using @Transactional, learned how to control transaction boundaries across service layers using propagation settings, configured rollback behavior for both checked and unchecked exceptions, and tested transactional behavior to maintain database integrity.

    This final step reinforces those learnings and outlines areas for deeper exploration. Understanding and mastering these transaction concepts enables you to build resilient applications that safeguard data consistency and recover gracefully from failures.


    Summary:

    In this lab, you:

    • Enabled declarative transaction support with @Transactional
    • Explored transaction propagation (REQUIRES_NEW, REQUIRED, NESTED)
    • Configured rollback behavior for checked exceptions
    • Used transactional annotations in test cases for data isolation

    Suggestions for Further Learning:

    • Transaction Isolation Levels: Explore how different isolation levels like READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, and SERIALIZABLE affect concurrent access to data.
    • PlatformTransactionManager: Learn how Spring delegates transaction handling to specific implementations like JpaTransactionManager or DataSourceTransactionManager.
    • TransactionTemplate: Use this for programmatic transaction management when declarative style does not provide enough control.

    With these foundational skills, you are well-prepared to handle complex transaction scenarios in real-world enterprise Spring Boot applications.

About the author

Amar Sonwani is a software architect with more than twelve years of experience. He has worked extensively in the financial industry and has expertise in building scalable applications.

Real skill practice before real-world application

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Learn by doing

Engage hands-on with the tools and technologies you’re learning. You pick the skill, we provide the credentials and environment.

Follow your guide

All labs have detailed instructions and objectives, guiding you through the learning process and ensuring you understand every step.

Turn time into mastery

On average, you retain 75% more of your learning if you take time to practice. Hands-on labs set you up for success to make those skills stick.

Get started with Pluralsight