Transactional Annotation in Spring Boot
Introduction#
The @Transactional
annotation in Spring Boot manages database transactions declaratively. It ensures that operations within a method are either fully committed or rolled back based on success or failure, adhering to ACID principles.
- Automatic Rollback: Rolls back on exceptions.
- Isolation Levels: Supports different isolation levels (e.g.,
READ_COMMITTED
). - Propagation Behavior: Defines transaction behavior in relation to other transactions (e.g.,
REQUIRED
). - Read-only Transactions: Optimizes performance by marking transactions as read-only when no updates are needed.
The @Transactional
annotation in Spring can be compared to managing a group project report:
- Automatic Rollback: Discard drafts if errors are found (rollback on exceptions).
- Isolation Levels: Team members avoid overwriting each other’s work (prevent conflicts in transactions).
- Propagation Behavior: Subtasks depend on the main task's success (manage nested transactions).
- Read-only Transactions: Review the report without editing (optimize performance for read-only operations).
Spring Transactional Context#
When a method is annotated with @Transactional
, Spring manages the entire transaction lifecycle. It ensures that the database operations within that method are executed as a single unit, maintaining consistency and reliability. Here's how it works:
Key Steps in Transactional Context:#
- Beginning a Transaction:
- Before the method starts, Spring opens a database connection and begins a new transaction.
- Committing the Transaction:
- If the method completes successfully without throwing an exception, Spring commits the transaction, making all changes permanent in the database.
- Rolling Back the Transaction:
- If the method throws an unchecked exception (like
RuntimeException
orError
), Spring will roll back the transaction, reverting all changes to the state before the transaction began.
- If the method throws an unchecked exception (like
When a method is annotated with @Transactional
, it ensures database operations within that method are executed as a single unit. Using a shopping trip analogy:
- Beginning a Transaction: Starting the shopping trip and picking up a cart (open connection).
- Committing the Transaction: Successfully buying all items and checking out (save changes).
- Rolling Back the Transaction: Leaving the store without purchasing anything due to an issue (revert changes).
How It Works - Proxy-based AOP:#
- Aspect-Oriented Programming (AOP): Spring uses AOP with proxying to manage transactions.
- Before the Method: When the method is called, Spring wraps it in a proxy that starts a transaction.
- After the Method: When the method finishes, the proxy commits or rolls back the transaction based on whether an exception was thrown.
Transactional Propagation#
The propagation attribute of the @Transactional
annotation defines how a transaction should behave in relation to other existing transactions. It determines the way transactions are handled when a method is called within an existing transaction or when no transaction is present.
Here are the possible propagation values in Spring:
- REQUIRED: Joins an existing transaction or creates a new one if none exists (default).
- REQUIRES_NEW: Always starts a new transaction, suspending any existing one.
- MANDATORY: Must run within an existing transaction; throws an exception if none exists.
- SUPPORTS: Runs within an existing transaction if available, otherwise runs without a transaction.
- NOT_SUPPORTED: Runs without a transaction, suspending any existing transaction.
- NESTED: Starts a nested transaction if one exists, or behaves like
REQUIRED
if no transaction exists. - NEVER: Must not run within a transaction; throws an exception if one exists.
Transactional propagation can be compared to handling tasks during team meetings:
- REQUIRED: Join an existing meeting or start a new one (default).
- REQUIRES_NEW: Start a separate meeting, pausing the current one.
- MANDATORY: Work only if a meeting is in progress; fail otherwise.
- SUPPORTS: Join the meeting if it exists, or work independently.
- NOT_SUPPORTED: Avoid working in a meeting; wait for it to finish.
- NESTED: Start a sub-meeting within the main meeting or start a new one.
- NEVER: Refuse to work if a meeting is in progress.
Transactional Isolation in Spring#
In Spring, the isolation level of a transaction defines how data is isolated between multiple transactions, ensuring the correct level of concurrency control. It determines the degree to which one transaction's modifications are visible to others.
- DEFAULT: Uses the database's default isolation level (usually
READ_COMMITTED
). - READ_UNCOMMITTED: Allows reading uncommitted data, leading to dirty reads.
- READ_COMMITTED: Prevents dirty reads but allows non-repeatable reads.
- REPEATABLE_READ: Prevents both dirty and non-repeatable reads but might allow phantom reads.
- SERIALIZABLE: The strictest isolation level, ensuring no dirty reads, non-repeatable reads, or phantom reads, but impacts performance.
Implementation:#
Entity Code:
Repository Code:
Controller Code: Create a put mapping that incrementing the balance by 1.
Service Code: No @Transactional
is used, operating under default READ_COMMITTED
isolation.
Output:
Concurrency Test Output#
(Concurrency test output is like ensuring all transactions in a bank are processed correctly even when many customers are withdrawing at the same time, without causing errors or delays. It checks if each transaction is completed properly, without conflicts or failures.)
Output of bash terminal:
After creating the employee if we are trying to hit put mapping for 100 times using the command. This command uses seq to make the PUT curl request 100 times. It uses xargs -P flag to call 10 curl requests at a time.
Command#
Run 100 concurrent requests to increment the balance:
seq 100 | xargs -P 10 -I {} curl --location --request PUT '<http://127.0.0.1:8080/employees/incrementBalance/1'
>
Alternatively, You can also use ApacheBench (installation may be required) ab -n 100 -c 10 -T 'application/json' -m PUT <http://127.0.0.1:8080/items/incrementCount/1
>
If you are not handling the exception you will get this exception(Internal Server).
The behavior is caused by concurrent access and lack of synchronization mechanisms during updates to the balance. Key points:
- Concurrent Reads and Writes: Multiple threads read the same initial state (
balance=1.00, version=1
) and try to update it simultaneously. - Outdated Data Overwriting: Threads using stale data (outdated
version
) overwrite newer updates, causing anomalies like the balance "jumping backward." - Stale Data Errors: Version mismatches lead to
Stale data
errors when conflicting updates occur. - No Transactions or Locking: Without transactional boundaries or optimistic locking, consistency cannot be maintained during concurrent updates.
Additional causes include potential caching issues, network delays, or concurrency conflicts leading to inconsistent or outdated data being processed.
Output of ide console: If you are not handling the exception you will get this exception.
Conclusion#
This article discussed the @Transactional annotation in Spring Boot, explaining its role in managing database transactions with automatic rollback, isolation levels, and propagation behaviors. It emphasized how @Transactional ensures atomicity, data consistency, and prevents race conditions. The article highlighted how Spring simplifies transaction management, ensuring data integrity and optimizing performance in multi-user scenarios.