1. Overview
Spring’s @Transactional annotation provides a nice declarative API to mark transactional boundaries.
Behind the scenes, an aspect takes care of creating and maintaining transactions as they are defined in each occurrence of the @Transactional annotation. This approach makes it easy to decouple our core business logic from cross-cutting concerns such as transaction management.
In this tutorial, we’ll see that this isn’t always the best approach. We’ll explore what programmatic alternatives Spring provides, such as TransactionTemplate, and our reasons for using them.
2. Trouble in Paradise
Let’s suppose we’re mixing two different types of I/O in a simple service:
@Transactional
public void initialPayment(PaymentRequest request) {
savePaymentRequest(request); // DB
callThePaymentProviderApi(request); // API
updatePaymentState(request); // DB
saveHistoryForAuditing(request); // DB
}
Here we have a few database calls alongside a possibly expensive REST API call. At first glance, it might make sense to make the whole method transactional since we may want to use one EntityManager to perform the whole operation atomically.
However, if that external API takes longer than usual to respond for whatever reason, we may soon run out of database connections!
2.1. The Harsh Nature of Reality
Here’s what happens when we call the initialPayment method:
- The transactional aspect creates a new EntityManager and starts a new transaction, so it borrows one Connection from the connection pool.
- After the first database call, it calls the external API while keeping the borrowed Connection.
- Finally, it uses that Connection to perform the remaining database calls.
If the API call responds very slowly for a while, this method would hog the borrowed Connection while waiting for the response.
Imagine that during this period we get a burst of calls to the initialPayment method. In that case, all Connections may wait for a response from the API call. That’s why we may run out of database connections — because of a slow back-end service!
Mixing the database I/O with other types of I/O in a transactional context isn’t a great idea. So, the first solution for these sorts of problems is to separate these types of I/O altogether. If for whatever reason we can’t separate them, we can still use Spring APIs to manage transactions manually.
3. Using TransactionTemplate
TransactionTemplate provides a set of callback-based APIs to manage transactions manually. In order to use it, we should first initialize it with a PlatformTransactionManager.
We can set up this template using dependency injection:
// test annotations
class ManualTransactionIntegrationTest {
@Autowired
private PlatformTransactionManager transactionManager;
private TransactionTemplate transactionTemplate;
@BeforeEach
void setUp() {
transactionTemplate = new TransactionTemplate(transactionManager);
}
// omitted
}
The PlatformTransactionManager helps the template to create, commit or roll back transactions.
When using Spring Boot, an appropriate bean of type PlatformTransactionManager will be automatically registered, so we just need to simply inject it. Otherwise, we should manually register a PlatformTransactionManager bean.
3.1. Sample Domain Model
From now on, for the sake of demonstration, we’re going to use a simplified payment domain model.
In this simple domain, we have a Payment entity to encapsulate each payment’s details:
@Entity
public class Payment {
@Id
@GeneratedValue
private Long id;
private Long amount;
@Column(unique = true)
private String referenceNumber;
@Enumerated(EnumType.STRING)
private State state;
// getters and setters
public enum State {
STARTED, FAILED, SUCCESSFUL
}
}
Also, we’ll initialize TransactionTemplate instance before each test case:
@DataJpaTest
@ActiveProfiles("test")
@Transactional(propagation = NOT_SUPPORTED) // we're going to handle transactions manually
public class ManualTransactionIntegrationTest {
@Autowired
private PlatformTransactionManager transactionManager;
@Autowired
private EntityManager entityManager;
private TransactionTemplate transactionTemplate;
@BeforeEach
public void setUp() {
transactionTemplate = new TransactionTemplate(transactionManager);
}
// tests
}
3.2. Transactions With Results
The TransactionTemplate offers a method called execute, which can run any given block of code inside a transaction and then return some result:
@Test
void givenAPayment_WhenNotDuplicate_ThenShouldCommit() {
Long id = transactionTemplate.execute(status -> {
Payment payment = new Payment();
payment.setAmount(1000L);
payment.setReferenceNumber("Ref-1");
payment.setState(Payment.State.SUCCESSFUL);
entityManager.persist(payment);
return payment.getId();
});
Payment payment = entityManager.find(Payment.class, id);
assertThat(payment).isNotNull();
}
Here we’re persisting a new Payment instance into the database and then returning its auto-generated id.
Similar to the declarative approach, the template can guarantee atomicity for us.
If one of the operations inside a transaction fails to complete, it rolls back all of them:
@Test
void givenTwoPayments_WhenRefIsDuplicate_ThenShouldRollback() {
try {
transactionTemplate.execute(status -> {
Payment first = new Payment();
first.setAmount(1000L);
first.setReferenceNumber("Ref-1");
first.setState(Payment.State.SUCCESSFUL);
Payment second = new Payment();
second.setAmount(2000L);
second.setReferenceNumber("Ref-1"); // same reference number
second.setState(Payment.State.SUCCESSFUL);
entityManager.persist(first); // ok
entityManager.persist(second); // fails
return "Ref-1";
});
} catch (Exception ignored) {}
assertThat(entityManager.createQuery("select p from Payment p").getResultList()).isEmpty();
}
Since the second referenceNumber is a duplicate, the database rejects the second persist operation, causing the whole transaction to roll back. Therefore, the database does not contain any payments after the transaction.
It’s also possible to manually trigger a rollback by calling the setRollbackOnly() on TransactionStatus:
@Test
void givenAPayment_WhenMarkAsRollback_ThenShouldRollback() {
transactionTemplate.execute(status -> {
Payment payment = new Payment();
payment.setAmount(1000L);
payment.setReferenceNumber("Ref-1");
payment.setState(Payment.State.SUCCESSFUL);
entityManager.persist(payment);
status.setRollbackOnly();
return payment.getId();
});
assertThat(entityManager.createQuery("select p from Payment p").getResultList()).isEmpty();
}
3.3. Transactions Without Results
If we don’t intend to return anything from the transaction, we can use the TransactionCallbackWithoutResult callback class:
@Test
void givenAPayment_WhenNotExpectingAnyResult_ThenShouldCommit() {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
Payment payment = new Payment();
payment.setReferenceNumber("Ref-1");
payment.setState(Payment.State.SUCCESSFUL);
entityManager.persist(payment);
}
});
assertThat(entityManager.createQuery("select p from Payment p").getResultList()).hasSize(1);
}
3.4. Custom Transaction Configurations
Up until now, we used the TransactionTemplate with its default configuration. Although this default is more than enough most of the time, it’s still possible to change configuration settings.
Let’s set the transaction isolation level:
transactionTemplate = new TransactionTemplate(transactionManager);
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ);
Similarly, we can change the transaction propagation behavior:
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
Or we can set a timeout, in seconds, for the transaction:
transactionTemplate.setTimeout(1000);
It’s even possible to benefit from optimizations for read-only transactions:
transactionTemplate.setReadOnly(true);
Once we create a TransactionTemplate with a configuration, all transactions will use that configuration to execute. So, if we need multiple configurations, we should create multiple template instances.
4. Using PlatformTransactionManager
In addition to the TransactionTemplate, we can use an even lower-level API such as PlatformTransactionManager to manage transactions manually. Quite interestingly, both @Transactional and TransactionTemplate use this API to manage their transactions internally.
4.1. Configuring Transactions
Before using this API, we should define how our transaction is going to look.
Let’s set a three-second timeout with the repeatable read transaction isolation level:
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
definition.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ);
definition.setTimeout(3);
Transaction definitions are similar to TransactionTemplate configurations. However, we can use multiple definitions with just one PlatformTransactionManager.
4.2. Maintaining Transactions
After configuring our transaction, we can programmatically manage transactions:
@Test
void givenAPayment_WhenUsingTxManager_ThenShouldCommit() {
// transaction definition
TransactionStatus status = transactionManager.getTransaction(definition);
try {
Payment payment = new Payment();
payment.setReferenceNumber("Ref-1");
payment.setState(Payment.State.SUCCESSFUL);
entityManager.persist(payment);
transactionManager.commit(status);
} catch (Exception ex) {
transactionManager.rollback(status);
}
assertThat(entityManager.createQuery("select p from Payment p").getResultList()).hasSize(1);
}
5. Conclusion
In this article, we first saw when we should choose programmatic transaction management over the declarative approach.
Then, by introducing two different APIs, we learned how to manually create, commit or roll back any given transaction.
As usual, the sample code is available over on GitHub.