The Middle Way: Annotation-based Transaction Management

Rahul Prabhakar
WorkMarket Engineering
4 min readFeb 13, 2018

--

The WorkMarket Platform (Work OS) has been in development since 2010. Naturally, the codebase has seen its fair share of stratification over the years with the introduction of new technologies and engineer perspectives. Database transaction management is one such strata that’s seen multiple implementations. This post will focus on a key subsets of these implementations, comparing their respective pros and cons, and sharing some takeaways and lessons learned.

Database Transaction Management

A database (DB) transaction is a sequence of operations performed as a single “all or nothing” unit of work against a database system. Our platform, comprising of a monolithic application and various microservices, is written using the Spring and Spring Boot frameworks, respectively. Both frameworks provide two different approaches to database transaction management: declarative and programmatic.

Programmatic

The programmatic approach requires explicit transaction management code alongside business logic. Here’s a sample snippet from a microservice I wrote a while back:

Notice line 225 “writeDataSource.runInTransaction(requestContext, ((subscriber, connectionOrDatasource)” is our transaction management code. It lives alongside our microservice business logic (lines 226 to 230).

A clear advantage here is the absence of framework magic. As DB transaction logic is explicitly stated, any engineer can look at this code and understand when the transaction begins and ends.

Another advantage is its flexibility. Programmatic transaction APIs provide more fine-grained control over the properties, lifespan, and error-handling of a DB transaction.

However, explicit transaction code introduces a lot of boilerplate. More code means larger cost of maintenance over the lifetime of the codebase (via bug fixing and code augmentation).

As microservices are smaller in scope and have fewer transactional operations, boilerplate is manageable at this scale. The programmatic approach is an appropriate choice for microservice development.

Declarative

The declarative approach allows transaction logic to live outside of our business logic through the use of a configuration file (e.g. XML config file) or annotations.

Here’s a small section of a XML config file from our monolith app that shows the setup of our read-only transaction advice (markup code that specifies the properties of our transactions) and the “businessServiceMethods” AOP (aspect-oriented programming) pointcut (markup code that specifies where to apply our transaction wrapping):

If we were to apply the “readOnlyTxAdvice” (lines 17–21) to all methods captured in our “businessServiceMethods” regular expression (lines 26–28), all captured methods would be wrapped in a read-only DB transaction.

In contrast to the programmatic approach, the properties of the transaction and the start-end boundaries are decoupled from the business logic. Any engineer can add a new method to these managed packages and expect them to be wrapped in a read-only DB transaction. Explicit transaction code is not required.

However, as a long-term solution, I would not recommend the config file approach. Completely isolating transaction management code from business logic can produce some unintended consequences. For instance, transactions tend to become large and bloated because engineers can’t easily see that their business service methods are living within a transaction boundary. It’s an understandable mistake given, just by looking at the business logic alone, there’s no obvious way to differentiate between a transactional and non-transactional method.

Declarative with Annotations

An alternative declarative solution is the annotation-based approach. Here’s an example of using Spring’s “@Transactional” annotation to specify that read-only transactions should wrap methods of the “DefaultFooService” class:

The benefit of this approach is that you can specify the transaction boundaries in code (similar to the programmatic approach) while keeping transaction and business logic cleanly separated with minimal boilerplate. For instance, the code above shows that all of DefaultFooService’s methods are wrapped in a read-only transaction (using the “@Transactional(readOnly = true)” annotation at the top of the file). The “updateFoo” method, on the other hand, is further customized to create a new read-write transaction when triggered (using properties “readOnly = false” and “Propagation.REQUIRES_NEW”). The annotations-based approach benefits engineers by taking the middle way: adding transaction logic along side business logic while minimizing boilerplate.

Concluding Remarks

For your next Spring-based project (that requires a transaction management system), here are my recommendations:

  • If your codebase will remain small and require few transaction based operations (e.g. a microservice), take the programmatic approach.
  • Otherwise, take the annotations based declarative approach.
  • Avoid config file based transaction management, or use at your own peril

--

--