How to manage Transactions in Quarkus

This article launches you on a tour of Transaction Management in Quarkus applications by focusing on the standard declarative approach and the new programmatic transaction API.

Quarkus narayana extension

Quarkus, just like other Enterprise applications, uses a Transaction Manager to coordinate and expose transactions to your applications. At its core, Quarkus uses Narayana Transaction Manager, which should be familiar to you if you have experience with WildFly or JBoss Enterprise Application Platform.

Narayana is a transactions toolkit which provides support for a broad range of transaction protocol such as JTA. Narayana Transaction Manager is automatically added to your dependencies if you include Hibernate ORM in your application. As a proof of concept, check the dependency tree of an Hibernate ORM application:

mvn dependency:tree
. . . . .
[INFO] +- io.quarkus:quarkus-hibernate-orm:jar:2.8.0.Final:compile
[INFO] |  |  +- org.jboss.narayana.jta:narayana-jta:jar:5.12.4.Final:compile
[INFO] |  |  |  +- org.jboss:jboss-transaction-spi:jar:7.6.0.Final:compile
[INFO] |  |  |  \- org.jboss.spec.javax.resource:jboss-connector-api_1.7_spec:jar:1.0.0.Final:compile
[INFO] |  |  +- org.jboss.narayana.jts:narayana-jts-integration:jar:5.12.4.Final:compile

If you don’t include Hibernate as a dependency, you will have to add manually the Narayana extension to your application:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-narayana-jta</artifactId>
</dependency>

A key setting of every Transactional Application is the Transaction Timeout. The default value of it is 60 seconds. However, you can change it in your configuration through the property quarkus.transaction-manager.default-transaction-timeout :

quarkus.transaction-manager.default-transaction-timeout=120

In the following example, we will show how to set the Transaction Timeout in a more granular way, on single application methods.

Building a sample Transactional Application

To demonstrate how you can manage Transactions with Quarkus we will build a simple REST application. Firstly, we will add the Model which we will persist on the Database:

@NamedQuery(name = "Ticket.findAll", query = "SELECT t FROM Ticket t")
@Entity
public class Ticket  {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	@Column
	public String name;

	@Column
	public String seat;

       // getter/setters omitted for brevity

}

Next, let’s code the TicketService Class which persists and fetches items from the Database:

@RequestScoped
public class TicketService {

	@Inject
	EntityManager em;

	@Transactional
	@TransactionConfiguration(timeout = 1000)
	public void buyTicket(Ticket ticket) {
		em.persist(ticket);
		System.out.println("Ticket created");
	}


	public Ticket buyTicketProgrammaticTransaction(Ticket ticket) {
		QuarkusTransaction.begin();
		em.persist(ticket);

		if (!ticket.getName().equals("scam")) {
			QuarkusTransaction.commit();
			System.out.println("Commit!");
			return ticket;
		}
		else {
			QuarkusTransaction.rollback();
			System.out.println("Rollback!");
			return null;
		}
		
	}
	public List<Ticket> listTickets() {
		return em.createNamedQuery("Ticket.findAll", Ticket.class)
				.getResultList();
	}
}

As you can see, the above class shows two approaches for managing Transactions:

Managing Transactions Declaratively

This is the standard way to handle Transactions in CDI applications. The @Transactional annotation provides the application the ability to declaratively control transaction boundaries on CDI Beans. You can apply it both at class and method level. Method level annotations override those at the class level. To learn more about CDI and Transactions check this article: Using Transactions in CDI Beans

Next, please note we have included the @io.quarkus.narayana.jta.runtime.TransactionConfiguration annotation to specify the Transaction Timeout for the method buyTicket.

Finally, to specify how to Transaction Manager manages a new Transaction you can specify an attribute in the @Transactional annotation. The default value is REQUIRED which starts a transaction if none was active. It uses the existing one otherwise.

@Transactional(SUPPORTS)

Managing Transactions Programmatically

The method buyTicketProgrammaticTransaction shows how to use the new io.quarkus.narayana.jta.QuarkusTransaction API to handle transactions programmatically.

Please note, this feature requires Quarkus 2.8.0 or newer.

The io.quarkus.narayana.jta.QuarkusTransaction can use a set of static methods to demarcate the boundaries of transactions:

  • QuarkusTransaction.begin(): demarcates the beginning of a JTA Transaction.
  • QuarkusTransaction.commit(): commits a Transaction
  • QuarkusTransaction.rollback(): marks the Transaction for Rollback

In our method buyTicketProgrammaticTransaction we are using the content of a field name to determine if the Transaction should commit or rollback.

Exposing the Transaction Service

Next, to complete our example, we will add a REST Endpoint which maps the two methods to buy a Ticket and one to fetch the existing Tickets:

@Path("/tickets")
public class TicketResource {
	@Inject
	TicketService service;
	
	@POST
	@Path("/buy1")
	public Response buyTicket1(Ticket ticket) {
		service.buyTicket(ticket);
		return Response.ok(ticket).status(201).build();
	}

	@POST
	@Path("/buy2")
	public Response buyTicket2(Ticket ticket) {
		ticket = service.buyTicketProgrammaticTransaction(ticket);
		System.out.println(ticket);
		if (ticket == null) {
			 throw new WebApplicationException("Ticket invalid", 500);	
		}
		 return Response.ok(ticket).status(201).build();
	}

	@GET
	public List<Ticket> ListTickets() {
		return service.listTickets();
	}
}

Finally, with the application up and running, you can start testing it:

curl -X POST http://localhost:8080/tickets/buy1 -H 'Content-Type: application/json' -d '{"name":"john","seat":"abc"}'

curl -X POST http://localhost:8080/tickets/buy2 -H 'Content-Type: application/json' -d '{"name":"frank","seat":"def"}'

curl -X POST http://localhost:8080/tickets/buy2 -H 'Content-Type: application/json' -d '{"name":"scam","seat":"def"}'

As you can see from the Console, the last POST will cause the TransactionManager to rollback the Transaction:

quarkus transactions

Handling Transaction Exceptions

You might have noticed that our TicketService Class does not use any try/catch semantic to handle the Transaction outcome (commit/rollback). As a matter of fact, when using the io.quarkus.narayana.jta.QuarkusTransaction API, it is up to you to throw or handle Exceptions in response to Transaction outcomes if your application needs it.

Also, keep in mind that all transactions are bound to the CDI Context. Therefore, all active transactions will be automatically be rolled back when the CDI Context is destroyed. For example with @RequestScoped Beans when the Request is over.

Within the Product documentation, you can check how to use Lambda Scoped Transactions to handle exceptions and transaction semantics using Lambda Expressions and io.quarkus.narayana.jta.QuarkusTransaction API,.

Finally, If you prefer the legacy approach, you can still inject the UserTransaction object in your Quarkus applications:

@Inject UserTransaction transaction;
public Ticket buyTicketLegacyTransaction(Ticket ticket) throws Exception {
    try {
        transaction.begin();
        em.persist(ticket);
        transaction.commit();
    }
    catch(Exception e) {
        // do something on Tx failure
        transaction.rollback();
    }
	return ticket; 

As you can see, when using the legacy UserTransaction object you have to handle or throw any Exception thrown by the UserTransaction API.

Source code

The source code for the example application is available here: https://github.com/fmarchioni/mastertheboss/tree/master/quarkus/transactions

Found the article helpful? if so please follow us on Socials