Generic Top

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

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 like transaction management.

In this tutorial, we'll see that this isn't always the best approach. We'll explore what programmatic alternatives Spring provides, like 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:

  1. The transactional aspect creates a new EntityManager and starts a new transaction – so, it borrows one Connection from the connection pool
  2. After the first database call, it calls the external API while keeping the borrowed Connection
  3. 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. Then, 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 is a bad smell. 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, first, we should initialize it with a PlatformTransactionManager. 

For example, 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 rollback 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 run all tests inside a test class, using the Testcontainers library to run a PostgreSQL instance before each test case:

@DataJpaTest
@Testcontainers
@ActiveProfiles("test")
@AutoConfigureTestDatabase(replace = NONE)
@Transactional(propagation = NOT_SUPPORTED) // we're going to handle transactions manually
public class ManualTransactionIntegrationTest {

    @Autowired 
    private PlatformTransactionManager transactionManager;

    @Autowired 
    private EntityManager entityManager;

    @Container
    private static PostgreSQLContainer<?> pg = initPostgres();

    private TransactionTemplate transactionTemplate;

    @BeforeEach
    public void setUp() {
        transactionTemplate = new TransactionTemplate(transactionManager);
    }

    // tests

    private static PostgreSQLContainer<?> initPostgres() {
        PostgreSQLContainer<?> pg = new PostgreSQLContainer<>("postgres:11.1")
                .withDatabaseName("baeldung")
                .withUsername("test")
                .withPassword("test");
        pg.setPortBindings(singletonList("54320:5432"));

        return pg;
    }
}

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. That is, 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 rollback. 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.

For example, we can 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);

Anyway, 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 like 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. For example, we can 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. Howeverwe 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 tutorial, first, we saw when one should choose programmatic transaction management over the declarative approach. Then, by introducing two different APIs, we learned how to manually create, commit, or rollback any given transaction.

As usual, the sample code is available over on GitHub.

Generic bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

Leave a Reply

avatar
  Subscribe  
Notify of