Transaction Management in Java EE Environments - A Comparative Analysis
This article describes various approaches to manage transactions for Java/JEE/open source technologies based applications. It also tabulates pros and cons of each.
A transaction is a logical unit of work comprising of several tasks; all or none of which must be performed in order to preserve data integrity. For example: A balance transfer from one bank account to another account is a transaction with two interdependent tasks viz. debit from one account and credit to another account.
begin transaction
debit account1 100
credit account2 100
commit transaction
For the transaction to be complete both the tasks must succeed.
Demarcation of a transaction or the transaction boundary restricts the tasks that need to be a part of one logical unit of work and therefore, need to be executed in a transaction. Primarily there are two ways to manage transactions either demarcate transactional boundaries programmatically in the code itself along with implementing the business logic or define it declaratively for the Container or Application Server to take care of it.
As per this approach developers programmatically begin, commit, or rollback transactions in the code using any of the following APIs:
Transaction with JDBC Example:
Connection connection = null;
Statement stmt = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
connection = getConnection();
connection .setAutoCommit(false);
stmt = connection.createStatement();
rs = stmt.executeQuery(some SELECT query);
// do something with result set.
processResultSet(rs);
// perform an update operation
pstmt = connection.prepareStatement(some UPDATE query);
pstmt.executeUpdate();
// commit the transaction
connection.commit();
} catch (SQLException ex) {
// rollback the transaction
try {
connection.rollback();
} catch (SQLException error) {
}
// log error.
} finally {
// close all resources such as rs, stmt, pstmt and connection.
}
JTA Example:
try {
UserTransaction tx = (UserTransaction)new InitialContext()
.lookup("java:comp/UserTransaction");
tx.begin();
// Do some work
load(...);
persist(...);
// . . . etc.
tx.commit();
}
catch (RuntimeException e) {
tx.rollback();
throw e; // or display error message
}
A code example depicting programmatic transaction demarcation with Hibernate Transaction API:
Session session = null;
Transaction tx = null;
try {
session = sessionFactory.openSession();
tx = session.beginTransaction();
// perform business logic here
doSomething(session);
tx.commit();
} catch (RuntimeException ex) {
try {
tx.rollback();
} catch (RuntimeException rEx) {
log.error("Couldn't roll back transaction", rEx);
}
throw ex;
} finally {
session.close();
}
The first approach is generally recommended. The second approach is similar to using the JTA UserTransaction API (although exception handling is less cumbersome).
Declaratively create a transaction assembly for example with annotation on methods. Its then the responsibility of the application deployer and the runtime environment to handle this concern. This can be done in two ways:
Declarative Example:
@Stateless
public class ManageAccountBean implements ManageAccount {
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public void updateAccount(Account account)throws AccountNotValidException {
// Merge account
account = getSessionFactory().getCurrentSession().update(account);
// do other stuff like notify account owner
...
}
...
}
When the method updateAccount() gets called, it executes with in a transactional context. At the end of execution of the method transaction gets committed or gets rolled back if any RuntimeException occur.
Six transactional attributes are possible for container-managed transaction demarcation.
Required: If there is no transaction, the container creates one; if there is one, it goes through. Upon returning, the container commits any transaction it started.

RequiresNew: The container always creates a new transaction, it always commits it.

Mandatory: If there is a transaction, the container propagates it. If not, the container throws an exception.

NotSupported: If a transaction is associated with the incoming call, the container stops the association and restarts the association upon leaving. If there is no transaction, the container does nothing.

Supports: If there is a transaction, the container propagates it. If not, the container does nothing.

Never: If there is a transaction the container throws an exception. If not, the container does nothing

Tables below compare various approaches of Transaction Management discussed above:
Table A: Comparison of Programmatic Vs Declarative Transaction Demarcation
|
Topic |
Pros |
Cons |
Comments |
|
Programmatic Transaction Demarcation |
|
|
Programmatic transaction management is usually a good idea only if we have a small number of transactional operations. For example, if we have a web application that require transactions only for certain update operations, we may not want to set up transactional proxies using Spring or any other technology. |
|
Declarative Transaction Demarcation |
|
|
If application has numerous transactional operations, declarative transaction management is usually worthwhile. It keeps transaction management out of business logic, and is not difficult to configure. |
Table B: Comparison of different APIs to achieve Programmatic Transaction Demarcation
|
Topic |
Pros |
Cons |
Comments |
|
Plain JDBC API |
|
|
It can be used for quick prototyping but is not recommended for production code. |
|
Standard JTA UserTransaction API |
|
|
|
|
Hibernate Transaction API |
|
|
|
|
JPA EntityTransaction API |
|
|
|
|
Spring framework |
|
|
|
Table C: Comparison of different ways of achieving Declarative Transaction Demarcation
|
Topic |
Pros |
Cons |
Comments |
|
EJB Container |
|
|
This is most widely used approach for applications deployed on Java EE server. |
|
Spring framework |
|
|
This approach is recommended if we want to have the flexibility of deploying the application with or without Java EE server. |
Most applications require execution of concurrent transactions. For example in a joint account husband and wife both can potentially update the balance at the same time. Applications inherit the isolation guarantees provided by the database management system. For example, Hibernate never locks anything in memory. On the other hand, Hibernate and Java Persistence features for pessimistic and optimistic concurrency control at the application level, can improve the isolation guarantee beyond what is provided by the database.
The standard isolation levels are defined by the ANSI SQL standard and JTA conforms to them. Increased level of isolation has higher cost and causes serious degradation of performance and scalability. Where as decreased level of isolation can cause data integrity issues such as dirty reads and data loss. The different isolation levels are:
Pessimistic locking is an approach where an entity is locked in the database for the entire time that it is in application memory (such as an Order object of an application). A lock either limits or prevents other users from working with the entity in the database. A write lock indicates that the holder of the lock intends to update the entity and disallows anyone from reading, updating, or deleting the entity. A read lock indicates that the holder of the lock does not want the entity to change while holding the lock, allowing others to read the entity but not update or delete it. The scope of a lock might be the entire database, a table, a collection of rows, or a single row. These types of locks are called database locks, table locks, page locks, and row locks respectively.
The advantages of pessimistic locking are that it is easy to implement and guarantees that your changes to the database are made consistently and safely. The primary disadvantage is that this approach isnt scalable. When a system has many users, or when the transactions involve a greater number of entities, or when transactions are long lived, then the chance of having to wait for a lock to be released increases. Therefore this limits the practical number of simultaneous users that a system can support.
With multi-user systems it is quite common to be in a situation where collisions are infrequent. For example the two of the customers are working with Order objects, one is working with the object O1 for item I1 while the other is working with the object O2 for item I2 and therefore they wont collide (Unless of course, if the database is explicitly set to do page level locking). When this is the case optimistic locking becomes a viable concurrency control strategy. The idea is that you accept the fact that collisions occur infrequently, and instead of trying to prevent them you simply choose to detect them and then resolve the collision when it does occur.
Figure and text below describes the logic for updating an object when optimistic locking is used:

Assuming hibernate will be used for O-R mapping and persistence, lets determine which isolation level and locking mechanism can serve best for most of the applications. We can easily eliminate read uncommitted and serializable isolation levels. Former is extremely dangerous as it allows one transactions uncommitted changes in a different transaction. Latter tends to scale very poorly and for most of the applications we do want concurrent transactions to occur as multiple concurrent users are expected to operate the system.
Now Hibernate can easily be configured to use versioned data. The combination of the (mandatory) persistence context cache and versioning (which allows us to resolve collision while using optimistic locking) already gives us most of the nice features of repeatable read isolation. In particular, versioning prevents the second lost updates problem, and the persistence context cache also ensures that the state of the persistent instance loaded by one transaction is isolated from changes made by other transaction. So, read committed isolation level for all database transaction can be acceptable if we use versioned data (optimistic locking). At the same time we can obtain a repeatable read guarantee explicitly in Hibernate for a particular transaction and piece of data with a pessimistic lock (such as using LockMode.UPGRADE which results in SQL SELECT FOR UPDATE or similar)
Table D: Locking and Isolation Level suggestions for an online trading application
|
Table Type |
Examples |
Suggested Locking Strategy |
Isolation Level |
|
Live-High Volume |
Order Item |
Optimistic (first choice) Pessimistic (second choice) |
Read Committed |
|
Live-Low Volume |
User |
Pessimistic (first choice) Optimistic (second choice) |
Read Committed |
|
Log (typically append only) |
Access_Log OrderHistory UserHistory |
Optimistic |
Read Committed |
|
Lookup/Reference (typically read only) |
State (as in 50 states of USA) |
Optimistic |
Read Committed |
For a distributed system consisting of multiple remote clients connected to a central server we can potentially start a transactional context at the client and propagate it to the server (remote transaction propagation). Or create a service layer / business layer on the server side where all the transactions start and end. Lets look at the pros and cons to help decide which can be used effectively for such an application.
Table A: Comparison of starting transactions from remote client vs. from local server
|
Topic |
Pros |
Cons |
Comments |
|
Transaction boundary at client side |
|
|
|
|
Transaction boundary on server side |
|
|
It is highly recommended by experts that Transaction demarcation and exception handling should be centralized. |
Assuming a distributed n-tier application which needs to support concurrent users and uses Spring/Hibernate:
This article compiles ideas from various soruces such as other web articles on Transactions and books on Java, J2EE, Hibernate, etc.