You don’t know @Transactional annotation, Do you?

Hari Tummala
8 min readMay 25, 2023

--

The @Transactional annotation is a powerful tool that simplifies the management of database transactions by automating transactional boundaries. It frees developers to concentrate on business logic while ensuring the integrity and consistency (ACID) of data operations.

Image taken from https://blog.ippon.tech/

In this blog post, we will explore the @Transactional annotation in detail, including its internals, features, and a deeper examination of isolations and propagation. To begin, let's consider an example that demonstrates how the @Transactional annotation is used.

@Transactional
public void processBatchData(List<DataRecord> records) {
for (DataRecord record : records) {
processRecord(record);
}
}

private void processRecord(DataRecord record) {
// Process and update record in the database
// ...
}

The above example is trying to perform a batch operation. Batch processing often involves performing a series of database operations on a large dataset. The @Transactional annotation can be used to ensure that all operations are processed atomically, maintaining data consistency. When the @Transactional annotation is applied to a method or a class, it typically signifies that a transaction should be initiated before the method begins executing and committed or rolled back after the method completes.

So what exactly happens when you use @Transactional annotation? —

  1. When using the @Transactional annotation, the framework creates a proxy around the target class or method. This proxy intercepts method invocations and adds transactional behavior. This mechanism allows the framework to dynamically control the transactional aspects of the annotated methods.
  2. The proxy intercepts method calls and applies a transactional aspect to the annotated methods. This aspect consists of pre-transactional, post-transactional, and exception-handling code that manages the lifecycle of the transaction.
  3. The @Transactional annotation interacts with a transaction manager, which is responsible for creating, committing, and rolling back transactions. The transaction manager coordinates with the underlying database or resource to ensure that the transactional operations are executed atomically.
  4. The @Transactional annotation defines the boundaries within which a method operates as a single transaction. When a method annotated with @Transactional is invoked, the transactional aspect intercepts the method call and starts a new transaction or joins an existing one based on the specified propagation behavior.

What features does @Transactional annotation provide? —

You can specify isolation level for the transaction: The isolation attribute of the @Transactional annotation defines the isolation level for the transaction. Isolation levels determine how the transaction interacts with concurrent transactions and how it preserves data integrity.

Here are the commonly used isolation levels in transaction processing and their explanations:

  1. DEFAULT (Default isolation level of the underlying database): The DEFAULT isolation level specifies that the default isolation level of the underlying database should be used. The actual default isolation level depends on the database system being used.
@Transactional(isolation = Isolation.DEFAULT)
public void performTransactionalOperation() {
// Transactional operation
// ...
}

In this example, the performTransactionalOperation() method is annotated with @Transactional(isolation = Isolation.DEFAULT), indicating that the default isolation level of the underlying database should be used for the transaction.

2. READ_UNCOMMITTED: The READ_UNCOMMITTED isolation level allows dirty reads, meaning a transaction can read uncommitted data from another transaction that has not yet been committed. This isolation level provides the least data integrity.

@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void performTransactionalOperation() {
// Transactional operation
// ...
}

In this example, the performTransactionalOperation() method is annotated with @Transactional(isolation = Isolation.READ_UNCOMMITTED), allowing the transaction to read uncommitted data from other concurrent transactions.

3. READ_COMMITTED: The READ_COMMITTED isolation level ensures that a transaction can only read data that has been committed by other transactions. It prevents dirty reads, but it allows non-repeatable reads and phantom reads.

@Transactional(isolation = Isolation.READ_COMMITTED)
public void performTransactionalOperation() {
// Transactional operation
// ...
}

In this example, the performTransactionalOperation() method is annotated with @Transactional(isolation = Isolation.READ_COMMITTED), ensuring that the transaction reads only committed data from other transactions.

4. REPEATABLE_READ: The REPEATABLE_READ isolation level ensures that a transaction can read the same data consistently throughout its lifespan. It prevents dirty reads and non-repeatable reads but allows phantom reads.

@Transactional(isolation = Isolation.REPEATABLE_READ)
public void performTransactionalOperation() {
// Transactional operation
// ...
}

In this example, the performTransactionalOperation() method is annotated with @Transactional(isolation = Isolation.REPEATABLE_READ), guaranteeing that the transaction sees consistent data throughout its execution.

5. SERIALIZABLE: The SERIALIZABLE isolation level provides the highest level of data integrity. It ensures that concurrent transactions are executed as if they were executed serially, avoiding dirty reads, non-repeatable reads, and phantom reads.

@Transactional(isolation = Isolation.SERIALIZABLE)
public void performTransactionalOperation() {
// Transactional operation
// ...
}

In this example, the performTransactionalOperation() method is annotated with @Transactional(isolation = Isolation.SERIALIZABLE), ensuring the highest level of data integrity by executing the transaction as if it were executed serially.

If you don’t mention isolation level, by default it uses the default isolation level of the underlying database. In PostgreSQL, the default isolation level is READ COMMITTED. This means that each transaction in PostgreSQL operates with a READ COMMITTED isolation level by default. When using the @Transactional annotation without specifying the isolation attribute, the transaction management framework will use the default isolation level of the underlying database, which, in the case of PostgreSQL, is READ COMMITTED.

You can specify timeout for the transaction: The timeout attribute of the @Transactional annotation specifies the maximum time allowed for the execution of a transaction before it times out and is rolled back. The timeout value is in seconds.

@Transactional(timeout = 60)
public void performTransactionalOperation() {
// Transactional operation
// ...
}

In this example, the performTransactionalOperation() method is annotated with @Transactional(timeout = 60), indicating that the transaction should time out and be rolled back if it takes longer than 60 seconds to complete.

You can mark a transaction as read only: The readOnly attribute of the @Transactional annotation defines whether the transaction is read-only or read-write. In read-only mode, the transaction is not allowed to modify any data, providing potential performance optimizations.

@Transactional(readOnly = true)
public void performReadOperation() {
// Read-only operation
// ...
}

In this example, the performReadOperation() method is annotated with @Transactional(readOnly = true), indicating that the transaction is read-only and should not modify any data.

You can set rollback rules on a transaction: The rollbackFor and noRollbackFor attributes of the @Transactional annotation specify the exceptions that should trigger a rollback or not trigger a rollback, respectively. These attributes allow fine-grained control over the transactional behavior in case of specific exceptions.

@Transactional(rollbackFor = {SQLException.class, CustomException.class})
public void performTransactionalOperation() {
// Transactional operation
// ...
}

In this example, the performTransactionalOperation() method is annotated with @Transactional(rollbackFor = {SQLException.class, CustomException.class}), indicating that if an SQLException or CustomException occurs, the transaction should be rolled back. Keep in mind that, if any other exception occurs (e.g., NullPointerException, RuntimeException, etc.), the transaction will not be rolled back automatically.

Transactional Propagation: The propagation attribute of the @Transactional annotation controls how transactions are propagated between methods. It determines whether a method should join an existing transaction, create a new transaction, or operate without a transaction based on the calling context. Here are some commonly used propagation attributes:

  1. REQUIRED Propagation:
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
// Transactional operation
// ...

methodB(); // Invoking methodB

// Transactional operation
// ...
}
@Transactional(propagation = Propagation.REQUIRED)
public void methodB() {
// Transactional operation
// ...
}

In this example, methodA() is annotated with @Transactional(propagation = Propagation.REQUIRED). When methodA() is invoked, it starts a new transaction. When it calls methodB(), the transaction created in methodA() is propagated to methodB(), meaning both methodA() and methodB() participate in the same transaction.

2. REQUIRES_NEW Propagation:

@Transactional(propagation = Propagation.REQUIRED)
public void methodC() {
// Transactional operation
// ...

methodD(); // Invoking methodD

// Transactional operation
// ...
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodD() {
// Transactional operation
// ...
}

In this example, methodC() is annotated with @Transactional(propagation = Propagation.REQUIRED), while methodD() is annotated with @Transactional(propagation = Propagation.REQUIRES_NEW). When methodC() is invoked, it starts a transaction. However, when it calls methodD(), a new transaction is started for methodD() independently, and the transaction in methodC() is suspended. Both methods operate within their own transactions.

3. MANDATORY Propagation:

@Transactional(propagation = Propagation.REQUIRED)
public void methodE() {
// Transactional operation
// ...

methodF(); // Invoking methodF

// Transactional operation
// ...
}
@Transactional(propagation = Propagation.MANDATORY)
public void methodF() {
// Transactional operation
// ...
}

In this example, methodE() is annotated with @Transactional(propagation = Propagation.REQUIRED), while methodF() is annotated with @Transactional(propagation = Propagation.MANDATORY). When methodE() is invoked, it starts a transaction. However, when it calls methodF(), it expects that a transaction is already active. If no transaction exists, an exception will be thrown.

4.NESTED Propagation:

@Transactional(propagation = Propagation.REQUIRED)
public void methodG() {
// Transactional operation
// ...

methodH(); // Invoking methodH

// Transactional operation
// ...
}
@Transactional(propagation = Propagation.NESTED)
public void methodH() {
// Transactional operation
// ...
}

In this example, methodG() is annotated with @Transactional(propagation = Propagation.REQUIRED), while methodH() is annotated with @Transactional(propagation = Propagation.NESTED). When methodG() is invoked, it starts a transaction. When it calls methodH(), a new nested transaction is created within the existing transaction. If no transaction exists, a new transaction is started for methodH().

5. SUPPORTS Propagation:

@Transactional(propagation = Propagation.REQUIRED)
public void methodI() {
// Transactional operation
// ...

methodJ(); // Invoking methodJ

// Transactional operation
// ...
}
@Transactional(propagation = Propagation.SUPPORTS)
public void methodJ() {
// Non-transactional operation
// ...
}

In this example, methodI() is annotated with @Transactional(propagation = Propagation.REQUIRED), while methodJ() is annotated with @Transactional(propagation = Propagation.SUPPORTS). When methodI() is invoked, it starts a transaction. When it calls methodJ(), the non-transactional method is executed without starting a new transaction.

6. NOT_SUPPORTED Propagation:

@Transactional(propagation = Propagation.REQUIRED)
public void methodK() {
// Transactional operation
// ...

methodL(); // Invoking methodL

// Transactional operation
// ...
}
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void methodL() {
// Non-transactional operation
// ...
}

In this example, methodK() is annotated with @Transactional(propagation = Propagation.REQUIRED), while methodL() is annotated with @Transactional(propagation = Propagation.NOT_SUPPORTED). When methodK() is invoked, it starts a transaction. When it calls methodL(), the transaction is suspended for the duration of methodL(), allowing the non-transactional operation to execute.

7. NEVER Propagation:

@Transactional(propagation = Propagation.REQUIRED)
public void methodM() {
// Transactional operation
// ...

methodN(); // Invoking methodN

// Transactional operation
// ...
}
@Transactional(propagation = Propagation.NEVER)
public void methodN() {
// Non-transactional operation
// ...
}

In this example, methodM() is annotated with @Transactional(propagation = Propagation.REQUIRED), while methodN() is annotated with @Transactional(propagation = Propagation.NEVER). When methodM() is invoked, it starts a transaction. When it calls methodN(), an exception is thrown because methodN() should not be executed in the presence of a transaction.

These examples demonstrate different propagation behaviors and how they affect the transactional behavior when one method calls another. By understanding and selecting the appropriate propagation behavior, you can control how transactions are propagated and managed within your application based on the specific requirements of each method.

Thank you for reading this story so far, if you like then please clap and share with your friends and colleagues.
If you have knowledge, let others light their candles in it. — Margaret Fuller

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Responses (4)

Write a response