Transactions & Concurrency
@Transactional — How Spring Manages Database Transactions
Dynamic proxies, rollback rules, and read-only attributes.
Introduction
Spring's declarative transaction management allows developers to control database transaction boundaries using the @Transactional annotation. Instead of writing manual JDBC commit/rollback statements, Spring intercepts calls and manages the transactional lifecycle.
Why It Matters
Database transactions guarantee **ACID properties** (Atomicity, Consistency, Isolation, Durability). Writing transaction logic manually results in duplicate boilerplate and resource leaks. @Transactional ensures that either all database queries inside a method succeed together, or all changes are rolled back cleanly if a failure occurs.
Real-World Analogy
Think of @Transactional like buying a plane ticket and reserving a hotel room together through a travel agent. If the flight booking completes but the hotel reservation fails, you do not want to be stuck with a ticket. The travel agent (Spring) acts as a transaction manager: if any step fails, they cancel all bookings (rollback) and return your money, ensuring you aren't left with an incomplete travel plan.
Detailed Mechanics
1. AOP Proxy Interception
At startup, Spring wraps any bean containing a @Transactional method in an **Aspect-Oriented Programming (AOP) proxy**. When an external class calls a transactional method, the proxy intercepts the execution, borrows a database connection from the pool, begins a transaction, and delegates execution to the actual service method.
2. Rollback Rules (Checked vs Unchecked)
By default, Spring rolls back transactions automatically only on **Unchecked Exceptions** (e.g. RuntimeException, NullPointerException, Error). If a method throws a **Checked Exception** (e.g. IOException, SQLException, or custom exceptions extending Exception), Spring commits the transaction anyway unless explicitly configured using the rollbackFor attribute.
3. Self-Invocation Caveat
Because transaction management relies on AOP proxies, calling a @Transactional method from another method in the **same class** (self-invocation) bypasses the proxy entirely. The target method runs directly as standard Java code, ignoring the annotation and failing to start a transaction.
Practical Example
Quick Quiz
Q1: Under what condition does a checked exception trigger a transactional rollback in Spring?
A) By default, checked exceptions always roll back.
B) Only if you run the app on PostgreSQL database.
C) Only if the @Transactional annotation declares rollbackFor = Exception.class (or specific checked exception classes).
D) Checked exceptions never roll back database modifications.
Answer: C — Declarative transactions default to rolling back on RuntimeExceptions. Checked exceptions require explicit rollbackFor rules.
Q2: Why does self-invocation inside a Spring bean ignore the @Transactional annotation?
A) Java does not support reflective calls inside classes.
B) The method call bypasses the Spring container proxy wrapper, invoking the method reference directly on the target instance.
C) Hibernate blocks transactions that do not originate from controllers.
D) Dynamic proxies are only active for interface classes.
Answer: B — Spring relies on proxy interceptors. Internal calls bypass the wrapper, executing without transaction context.
Scenario-Based Challenge
Production Scenario:
You configure a service method with @Transactional(rollbackFor = Exception.class). Inside the method, a database insertion runs, followed by a call to an external payment REST API. If the REST API times out and throws an exception, the database insertion is rolled back. However, the external payment API has already charged the client. How do you fix this transaction model?
This represents a mismatch between database transactions and remote network actions. Database transactions cannot roll back remote network modifications. To resolve this:
- Separate network calls: Execute the external REST call *before* opening the database transaction, or *after* the transaction has successfully committed.
- Idempotency & Compensation: If the payment API must run inside, save the transaction record with a "PENDING" status, invoke the REST call, and then update the status to "COMPLETED". If a timeout occurs, trigger a compensating transaction (refund API call) to reverse the remote change.
Interview Questions
1. Conceptual: How does @Transactional(readOnly = true) optimize performance?
It sets the database connection session to read-only, and instructs Hibernate to bypass dirty checking snapshot creation. Bypassing dirty checking saves significant memory allocation and CPU cycles during data loading.
2. Coding: How do you bypass the self-invocation limit if you must call a transactional method from within the same class?
The recommended clean approach is to refactor the transactional method into a separate helper Bean (e.g. InventoryUpdaterService). Alternatively, you can self-inject the proxy instance inside your class using setter injection or @Autowired:
Production Considerations
Keep transactional methods as brief as possible. Database connections are reserved for the duration of the transaction. Avoid embedding slow, blocking operations (like sending emails or making HTTP requests) inside transactional boundaries to prevent connection pool starvation under heavy loads.