Spring Transaction Management Overview

What is a Transaction?


A transaction in software systems is a group of operations that are executed as a single unit.  
A database transaction is any operation that is treated as a single unit of work that either completes fully or does not complete at all and leaves the storage system in a consistent state.

Transactions can impact a single record or multiple records.

Key Concepts in Transaction Management


1. ACID Properties:

ACID is an acronym that stands for atomicity, consistency, isolation, and durability.

ACID properties ensure that a database transaction (a set of read, write, update, or delete operations) leaves the database in a consistent state even in the event of unexpected errors.
  • Atomicity: Ensures that all operations within a transaction are completed successfully; if any operation fails, the entire transaction is rolled back.
  • Consistency: Ensures that a transaction brings the database from one valid state to another, maintaining database integrity.
  • Isolation: Ensures that transactions are executed in isolation from each other, preventing concurrent transactions from interfering with each other.
  • Durability: Ensures that once a transaction has been committed, the changes are permanent and will survive system failures like crashes or power outages.
2. Transaction Lifecycle:
  • Begin: The transaction starts.
  • Execute: Operations (such as queries and updates) are performed.
  • Commit: If all operations are successful, the transaction is committed, making all changes permanent.
  • Rollback: If any operation fails, the transaction is rolled back, undoing all changes made during the transaction.
3. Concurrency Control:
  • Locking: Mechanisms to prevent concurrent transactions from interfering with each other by controlling access to data (e.g., read locks, write locks).
  • Optimistic Concurrency Control: Transactions execute without locking resources but check for conflicts before committing.
  • Pessimistic Concurrency Control: Transactions lock resources before accessing them to prevent conflicts.
4. Transaction Isolation Levels:
  • Read Uncommitted: Transactions can read data that has not yet been committed by other transactions.
  • Read Committed: Transactions can only read data that has been committed by other transactions.
  • Repeatable Read: Ensures that if a transaction reads a value, it will read the same value throughout its execution.
  • Serializable: The highest isolation level, ensuring complete isolation from other transactions.


dirty read:
A transaction reads data written by a concurrent uncommitted transaction.

 nonrepeatable read:

A transaction re-reads data it has previously read and finds that data has been modified by another transaction (that committed since the initial read). non-repeatable read occurs when transaction A retrieves a row, transaction B subsequently updates the row, and transaction A later retrieves the same row again. Transaction A retrieves the same row twice but sees different data.

 phantom read:

A transaction re-executes a query returning a set of rows that satisfy a search condition and finds that the set of rows satisfying the condition has changed due to another recently-committed transaction. phantom read occurs when transaction A retrieves a set of rows satisfying a given condition, transaction B subsequently inserts or updates a row such that the row now meets the condition in transaction A, and transaction A later repeats the conditional retrieval. Transaction A now sees an additional row. This row is referred to as a phantom.

  serialization anomaly 

The result of successfully committing a group of transactions is inconsistent with all possible orderings of running those transactions one at a time.


Spring Transaction Management

Spring transaction management simply means: How does Spring start, commit, or rollback JDBC transactions. 
Spring provides both declarative (@Transactional) and programmatic (using a TransactionTemplate or PlatformTransactionManager) transaction management, and developers can choose based on the requirement.
Declarative transaction management is easier and more suitable in most cases, but in some cases, you want fine-grain control, and you can use programmatic transaction management.
In order to support transactions in a spring project, you need to add @EnableTransactionManagement to a @Configuration class.
However, if we're using a Spring Boot project and have spring-data-* or spring-tx dependencies on the classpath, then transaction management will be enabled by default.

The @Transactional Annotation

Any bean or public method annotated with the @Transactional annotation makes sure that the methods will be executed inside a database transaction.

The @Transactional annotation should be used in the service layer because it is this layer's responsibility to define the transaction boundaries.


How does the @Transactional annotation work ?

Spring creates dynamic proxies for classes that declare @Transactional on the class itself or on methods.

It does that through a method called proxy-through-subclassing with the help of the Cglib library.

It is also worth noting that the proxy itself does not handle these transactional states (open, commit, close); the proxy delegated this work to a transaction manager.

The proxy has access to a transaction manager and can ask it to open and close transactions and connections.

Spring offers a PlatformTransactionManager (extends TransactionManager) interface, which, by default, comes with a couple of handy implementations. One of them is the DataSourceTransactionManager.

Pitfalls

As Spring wraps the bean in the proxy, only calls from "outside" the bean are intercepted. That means, any self-invocation calls will not start any transaction, even if the method has the @Transactional annotation.

Moreover, only public methods should be annotated with @Transactional. Methods of any other visibility will silently ignore the annotation, as these are not proxying.

Transaction Rollback

By default, only RuntimeException and Error trigger a rollback. A checked exception does not trigger a rollback of the transaction.

The @Transactional annotation, on the other hand, supports rollbackFor or rollbackForClassName attributes for rolling back transactions, as well as noRollbackFor or noRollbackForClassName attributes for avoiding rollback.


The @Transactional annotation attributes

The @Transactional annotation provides the following attributes:

1) Propagation

The "propagation" attribute defines how the transaction boundaries propagate to other methods that will be called either directly or indirectly from within the annotated block.

@Service
public class OrderServiceI {

    @Transactional(propagation = Propagation.MANDATORY)
    public Long addOrder(OrderDto order) {
        // check inventory
        // make payment
        // create order
        return orderId;
    }
}

There are a variety of propagation modes that can be plugged into the @Transactional method.

                                                                                               
PropagationMeaning
REQUIREDThis is the default propagation. In this case, if no active transaction is found, spring creates one. Otherwise, the method appends to the currently active transaction:
SUPPORTSIf a transaction exists, then the method uses this existing transaction. If there isn't a transaction, it is executed non-transactional.
MANDATORYIf there is an active transaction, then it will be used. If there isn't an active transaction, then Spring throws an exception.
REQUIRES_NEWSpring suspends the current transaction if it exists and then creates a new one.
NOT_SUPPORTEDIf a current transaction exists, first Spring suspends it, and then the method runs without a transaction.
NEVERSpring throws an exception if there's an active transaction.
NESTEDSpring checks if a transaction exists, and if so, it marks a save point. If method execution throws an exception, then the transaction rolls back to this save point.
2) ReadOnly

The "readOnly" attribute defines if the current transaction is read-only or read-write.

@Service
public class OrderServiceI {
    @Transactional(readOnly = true)
    public Long addOrder(OrderDto order) {
        // check inventory
        // make payment
        // create order
        return orderId;
    }
}
3) RollbackFor and RollbackForClassName

The "rollbackFor" and "rollbackForClassName" attributes define one or more Throwable classes for which the current transaction will be rolled back.

By default, only RuntimeException and Error trigger a rollback. A checked exception does not trigger a rollback of the transaction.

@Service
public class OrderServiceI {

    @Transactional(rollbackFor = {SQLException.class},
            noRollbackFor = {EntityNotFoundException.class})
    public Long addOrder(OrderDto order) {
        // check inventory
        // make payment
        // create order
        return orderId;
    }
}

4) NoRollbackFor and NoRollbackForClassName

The "noRollbackFor" and "noRollbackForClassName" define one or more Throwable classes for which the current transaction will not be rolled back.

@Service
public class OrderServiceI {

    @Transactional(rollbackForClassName = {"NullpointerException"},
            noRollbackForClassName = {"EntityNotFoundException"})
    public Long addOrder(OrderDto order) {
        // check inventory
        // make payment
        // create order
        return orderId;
    }
}
 
Disabling Auto-Commit Mode


When a connection is created, it is in auto-commit mode. This means that each individual SQL statement is treated as a transaction and is automatically committed right after it is executed. (To be more precise, the default is for a SQL statement to be committed when it is completed, not when it is executed. A statement is completed when all of its result sets and update counts have been retrieved. In almost all cases, however, a statement is completed, and therefore committed, right after it is executed.)

The way to allow two or more statements to be grouped into a transaction is to disable the auto-commit mode. This is demonstrated in the following code, where con is an active connection:

        con.setAutoCommit(false);

connection.setAutoCommit(false) will allow you to group multiple subsequent Statements under the same transaction. This transaction will be committed when connection.commit() is invoked, as opposed to after each execute() call on individual Statements (which happens if autocommit is enabled).

Changing the auto-commit mode through connection.setAutoCommit() will implicitly commit the active transaction and create a new one. 
Spring AOP uses proxies to implement the additional transaction management behavior. This means that the actual business logic is wrapped in a proxy that handles transaction boundaries. While this approach simplifies transaction management, it also introduces certain limitations.

Common Scenarios Where @Transactional Might Fail


Self-Invocation
A common pitfall is self-invocation, where a method within the same class calls another method annotated with @Transactional. Since the proxy intercepts calls only from external clients, internal calls bypass the proxy and thus the transaction management.
@Service
public class OrderService {

@Autowired
private OrderRepository orderRepository;

public void placeOrder(Order order) {
saveOrder(order); // Direct internal call, bypassing proxy
}

@Transactional
public void saveOrder(Order order) {
orderRepository.save(order);
}
}
In this example, saveOrder is never run within a transactional context if called directly from placeOrder.
Solution: Use external method calls or refactor to use a different service class.

Spring AOP proxies only public methods by default. If you annotate a non-public method with @Transactional, it won't be transactional because the proxy won't intercept it.

@Service
public class UserService {

@Autowired
private UserRepository userRepository;

@Transactional
void updateUser(User user) { // Non-public method
userRepository.save(user);
}
}

Solution: Ensure that all transactional methods are public.

Proxy Types and Configuration

By default, Spring AOP creates proxies based on interfaces (JDK dynamic proxies). If your class does not implement an interface, Spring will use CGLIB proxies. Misconfigurations or misunderstandings about proxy types can lead to unexpected behavior.

Solution: Configure Spring to use class-based (CGLIB) proxies if needed by setting proxyTargetClass=true.

Misconfigured Proxy Settings

Improper proxy settings in your Spring configuration can also lead to issues. Make sure your Spring application context is set up to scan and process @Transactional annotations properly.

Solution: Verify your configuration, ensuring @EnableTransactionManagement is correctly set up.

Example

To avoid these pitfalls, consider the following example:

@Service
public class PaymentService {

@Autowired
private PaymentRepository paymentRepository;

@Transactional
public void processPayment(Payment payment) {
validatePayment(payment);
savePayment(payment);
}

@Transactional
public void savePayment(Payment payment) {
paymentRepository.save(payment);
}

private void validatePayment(Payment payment) {
// Validation logic
}
}
Detailed Explanation:
Public and Transactional Methods:
  • Both processPayment and savePayment methods are public and annotated with @Transactional. This ensures that when these methods are invoked from outside the PaymentService class, they are intercepted by the Spring AOP proxy, and transactional behavior is applied.

External Invocation of Transactional Methods:
  • When processPayment is called from an external class (or any other external source like a controller or another service), the Spring AOP proxy handles the method call. Since processPayment is marked as @Transactional, a transaction begins when this method is invoked.
  • Inside processPayment, the savePayment method is called. Because savePayment is also public and annotated with @Transactional, and because the call originates from a method that was invoked via the proxy, the proxy will correctly handle the transactional behavior of savePayment. As a result, the savePayment method will either participate in the existing transaction (if it is part of the same transaction as processPayment) or create a new transaction, depending on the @Transactional settings.

Avoiding Self-Invocation:
  • Since both processPayment and savePayment are public and transactional, there’s no self-invocation happening in this scenario. The call to savePayment from within processPayment does not bypass the proxy, because the whole processPayment method call itself was handled by the proxy when it was initially invoked from outside the class. This ensures that transactional behavior is properly applied to both methods.

Private Method for Non-Transactional Logic:
  • The validatePayment method is private and is not marked with @Transactional. This is intentional because validation typically doesn’t require transactional behavior. By keeping this method private and unannotated, you ensure that it’s only used within the context of processPayment, which is already transactional. This also keeps the logic clean, as it doesn’t need to be exposed or managed by the transactional proxy.

While @Transactional and Spring AOP simplify transaction management, understanding their limitations is crucial for effective use. By being aware of scenarios like self-invocation, non-public methods, proxy types, and configuration issues, you can avoid common pitfalls and ensure your transactions are managed as expected.

Post a Comment

Previous Post Next Post