Spring Transactions Part 1: Understanding How It Works Under the Hood

This is Part 1 of a 4-part series on Spring Transactions:
Imagine you're at a restaurant, and you order a burger and fries. The kitchen starts cooking both items simultaneously. Suddenly, they run out of fries. What happens? They don't serve you just the burger—they cancel the entire order and refund your money. Either everything succeeds, or nothing does.
This is the essence of transactions. In the world of databases, transactions ensure that a series of operations either all succeed or all fail together. No partial states, no corrupted data.
Spring Framework's @Transactional annotation is your go-to tool for managing transactions in Java applications. It's powerful, declarative, and deceptively simple. But beneath its simplicity lies a sophisticated mechanism that every Spring developer should understand.
In this first part, we'll peel back the layers and understand how Spring transactions work internally.
The "Why": A Quick ACID Refresher
Before we dive into Spring's implementation, let's ground ourselves with why transactions matter. Transactions adhere to ACID properties:
| Property | What It Means | Real-World Analogy |
|---|---|---|
| Atomicity | All operations succeed or all fail | The restaurant order: either you get everything, or you get nothing |
| Consistency | Database moves from one valid state to another | Your bank account balance is always correct |
| Isolation | Concurrent transactions don't interfere | Two people booking the same seat—one gets it, the other doesn't |
| Durability | Committed data survives failures | Once the payment is confirmed, it stays confirmed even if the server crashes |
Why Spring? Before Spring, transaction management was verbose and error-prone. You had to manually begin, commit, and rollback transactions while handling every edge case. Spring's @Transactional changed all that by introducing declarative transaction management.
How @Transactional Works Under the Hood
The @Transactional annotation is deceptively simple. You slap it on a method, and Spring magically handles transaction boundaries. But what's actually happening behind the scenes?
The AOP Proxy Pattern
Spring uses Aspect-Oriented Programming (AOP) to implement transaction management. When you annotate a method with @Transactional, Spring creates a proxy around your bean. This proxy intercepts method calls and manages the transaction lifecycle.
Here's the complete lifecycle from method invocation to commit/rollback:
How Spring Generates Proxies
Spring's proxy creation happens during the bean initialization phase through BeanPostProcessors. Here's what happens under the hood:
- Annotation Detection: Spring scans for
@Transactionalannotations during component scanning - Proxy Creation Decision:
AbstractAutoProxyCreatordetermines which beans need proxies based on advices and advisors - Proxy Generation: Either JDK dynamic proxy or CGLib proxy is created
- Bean Registration: The proxy is registered in the application context instead of the original bean
The key infrastructure components involved are:
// Spring's proxy creation infrastructure (simplified)
public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware {
// Decides whether a bean needs proxying
protected Object[] getAdvicesAndAdvisorsForBean(
Class<?> beanClass, String beanName, TargetSource targetSource) {
// Finds all @Transactional annotations
// Returns transaction advisor if found
}
// Creates the actual proxy
protected Object createProxy(Class<?> beanClass, String beanName,
Object[] specificInterceptors, TargetSource targetSource) {
// Chooses JDK or CGLib proxy
// Returns the proxy instance
}
}Crucial Understanding: When you inject a bean with @Transactional, Spring actually injects the proxy, not the original bean. This is why calling @Transactional methods from within the same class doesn't work—the call bypasses the proxy.
JDK Dynamic Proxy vs CGLib
Spring supports two proxy mechanisms, each with different characteristics:
JDK Dynamic Proxy
How it works:
- Creates a proxy class that implements the same interfaces as the target class
- Uses Java's built-in
java.lang.reflect.Proxyclass - Method calls are dispatched through
InvocationHandlerusing reflection
Requirements:
- The target class must implement at least one interface
- Only interface methods are proxied (not class-specific methods)
// JDK Proxy structure (conceptual)
public class $Proxy0 implements AccountService {
private final InvocationHandler handler;
public $Proxy0(InvocationHandler handler) {
this.handler = handler;
}
public TransferResult transferMoney(Long fromId, Long toId, BigDecimal amount) {
// Delegates to InvocationHandler
return handler.invoke(this, transferMoneyMethod, new Object[]{fromId, toId, amount});
}
}
// The InvocationHandler contains the transaction logic
class TransactionInvocationHandler implements InvocationHandler {
private final Object target; // Actual AccountService instance
public Object invoke(Object proxy, Method method, Object[] args) {
// 1. Begin transaction
// 2. Call target.method(args)
// 3. Commit or rollback
}
}Performance Characteristics:
- ✅ Faster proxy creation (no bytecode generation)
- ✅ Lower memory overhead
- ⚠️ Slightly slower method invocation (reflection-based)
CGLib Proxy
How it works:
- Creates a subclass of the target class at runtime
- Uses bytecode generation (ASM library under the hood)
- Overrides non-final methods to add interception logic
Requirements:
- No interface required
- Cannot proxy
finalclasses or methods - Requires a default constructor
// CGLib Proxy structure (conceptual)
public class AccountService$$EnhancerBySpringCGLIB$$12345678 extends AccountService {
private final MethodInterceptor interceptor;
// Intercepted method
public TransferResult transferMoney(Long fromId, Long toId, BigDecimal amount) {
// Spring's transaction interceptor
return interceptor.intercept(this, transferMoneyMethod, new Object[]{fromId, toId, amount}, null);
}
// Non-intercepted methods (like toString()) call super
public String toString() {
return super.toString();
}
}Performance Characteristics:
- ✅ Faster method invocation (direct calls, no reflection)
- ⚠️ Slower proxy creation (bytecode generation)
- ⚠️ Higher memory overhead
Comparison Table
| Aspect | JDK Dynamic Proxy | CGLib Proxy |
|---|---|---|
| Mechanism | Interface-based | Subclass-based |
| Requirement | Must implement interface | No interface needed |
| Constructor | Any constructor | Needs default/no-arg constructor |
| Final methods | Can proxy (if in interface) | Cannot proxy final methods/classes |
| Proxy Creation | Fast (uses reflection) | Slower (bytecode generation) |
| Method Invocation | Slower (reflection-based) | Faster (direct calls) |
| Spring Boot 2.x+ Default | Preferred when interface exists | Fallback for classes without interfaces |
Modern Spring Behavior: Since Spring Boot 2.x, the default is to use CGLib only when a class doesn't implement any interfaces. If your bean implements an interface, Spring will prefer JDK dynamic proxy. You can force CGLib globally with spring.aop.proxy-target-class=true.
The AOP Advice Chain
Transaction management is just one "cross-cutting concern." Spring AOP allows multiple concerns to be chained together through an advice chain. Each advisor in the chain can perform logic before and after the target method execution.
The advice chain executes in a specific order:
- Before Each Advisor: Each advisor's "before" logic runs
- Target Method: The actual business method executes
- After Each Advisor: Each advisor's "after" logic runs in reverse order
Example with Multiple Advisors:
@Service
public class AccountService {
@Transactional // Transaction advisor
@Secured("ROLE_USER") // Security advisor
@CacheEvict("accounts") // Caching advisor
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
// 1. Security check (first)
// 2. Cache eviction (second)
// 3. Transaction begin (third)
// 4. Business logic executes
// 5. Transaction commit/rollback (third - after)
// 6. Cache update (second - after)
// 7. Security logging (first - after)
}
}The order is controlled by @Order annotation or Ordered interface:
@Order(1) // Runs first, "unwinds" last
public class TransactionAdvisor implements Advisor {}
@Order(2) // Runs second, "unwinds" second
public class SecurityAdvisor implements Advisor {}The Transaction Interceptor Chain
The TransactionInterceptor is the workhorse that:
- Creates a transaction before method execution (using
PlatformTransactionManager) - Binds the transaction to the current thread
- Executes your business method
- Commits or rolls back based on the outcome
This happens automatically, without you writing a single line of transaction management code.
The Role of PlatformTransactionManager
PlatformTransactionManager is the abstraction that Spring uses to interact with different transaction APIs:
| Manager | Use Case |
|---|---|
| DataSourceTransactionManager | For JDBC |
| JpaTransactionManager | For JPA/Hibernate |
| JtaTransactionManager | For Java EE / distributed transactions |
Spring's Magic: The beauty is that your business code doesn't change whether you're using JDBC, JPA, or JTA. The @Transactional annotation works the same way across all of them.
Core Features & Benefits
Spring's declarative transaction management brings several powerful features to the table:
1. Declarative Transaction Management
Instead of writing verbose transaction management code, you simply annotate:
// The old way (programmatic)
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// business logic
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
}
// The Spring way (declarative)
@Transactional
public void transferMoney(Account from, Account to, BigDecimal amount) {
// business logic
}2. Automatic Commit/Rollback
Spring automatically commits on successful execution and rolls back on exceptions (with some nuances we'll cover in Part 2).
3. Exception Handling Rules
By default, Spring rolls back on unchecked exceptions (subclasses of RuntimeException) but not on checked exceptions. This behavior is configurable (more on this in Part 2).
4. Transaction Propagation Behaviors
Control how transactions interact with each other when methods call other transactional methods. We'll dive deep into all 7 propagation behaviors in Part 2.
5. Isolation Levels
Control how concurrent transactions see each other's changes. We'll explore all 5 isolation levels in Part 2.
Declarative vs Programmatic Transaction Management
Spring offers two approaches to transaction management. Understanding when to use each is crucial.
Declarative Transaction Management (Recommended)
This is what we've been discussing with @Transactional:
@Service
public class AccountService {
@Transactional
public void transferMoney(Account from, Account to, BigDecimal amount) {
accountRepository.debit(from, amount);
accountRepository.credit(to, amount);
}
}Advantages:
- Clean, readable code
- Separation of concerns
- Easy to maintain
- Less error-prone
Use when:
- Most business operations
- Standard transaction boundaries
- When you want clean, maintainable code
Programmatic Transaction Management
For fine-grained control, you can manage transactions programmatically:
@Service
public class AccountService {
private final PlatformTransactionManager transactionManager;
public void transferMoney(Account from, Account to, BigDecimal amount) {
TransactionDefinition def = new DefaultTransactionDefinition();
TransactionStatus status = transactionManager.getTransaction(def);
try {
accountRepository.debit(from, amount);
accountRepository.credit(to, amount);
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
throw e;
}
}
}Advantages:
- Fine-grained control
- Dynamic transaction behavior
- Can handle complex scenarios
Use when:
- You need dynamic transaction behavior based on runtime conditions
- Complex transaction scenarios that declarative can't handle
- Legacy code integration
Best Practice: Prefer declarative transactions (@Transactional) for 95% of cases. Only use programmatic when you absolutely need the additional control.
A Simple Example
Let's put it all together with a complete example:
@Service
@RequiredArgsConstructor
public class AccountService {
private final AccountRepository accountRepository;
@Transactional
public TransferResult transferMoney(Long fromId, Long toId, BigDecimal amount) {
Account from = accountRepository.findById(fromId)
.orElseThrow(() -> new AccountNotFoundException(fromId));
Account to = accountRepository.findById(toId)
.orElseThrow(() -> new AccountNotFoundException(toId));
// Business validation
if (from.getBalance().compareTo(amount) < 0) {
throw new InsufficientFundsException(from.getBalance(), amount);
}
// Perform transfer
from.debit(amount);
to.credit(amount);
accountRepository.save(from);
accountRepository.save(to);
return new TransferResult(from.getBalance(), to.getBalance());
}
}When transferMoney is called:
- Spring Proxy intercepts the call
- TransactionInterceptor creates a new transaction
- PlatformTransactionManager begins the database transaction
- Your method executes the business logic
- If successful → Commit
- If exception thrown → Rollback
All of this happens automatically!
What's Next?
Now that you understand how Spring transactions work internally, you're ready to dive deeper into the powerful configuration options available.
In Part 2, we'll explore:
- All
@Transactionalparameters in detail - The 7 transaction propagation behaviors with examples
- The 5 isolation levels and when to use each
- Rollback rules and exception handling
- Visual diagrams for decision-making
Continue to Part 2: Mastering @Transactional Parameters →
Key Takeaways from Part 1:
- Spring uses AOP proxies to intercept
@Transactionalmethods- Spring creates proxies during bean initialization via
BeanPostProcessors- JDK Dynamic Proxy: Interface-based, faster creation, requires an interface
- CGLib Proxy: Subclass-based, faster invocation, no interface required
- CGLib cannot proxy
finalclasses or methods—a common gotcha- Multiple advisors chain together for cross-cutting concerns (security, caching, logging)
- The
TransactionInterceptormanages the complete transaction lifecyclePlatformTransactionManagerabstracts different transaction APIs- Prefer declarative transactions for cleaner, more maintainable code
- Understanding proxy types helps debug transaction issues (e.g., self-invocation problems)
Tags
Related Posts

Spring Transactions Part 2: Mastering @Transactional Parameters
Deep dive into all @Transactional parameters: propagation behaviors, isolation levels, timeout, rollback rules, and when to use each configuration option.

Spring Transactions Part 3: Common Pitfalls and Best Practices
Learn the common pitfalls even experienced developers make with @Transactional and the best practices that separate novices from experts.

Spring Transactions Part 4: Production Optimization and Performance
Learn how to optimize Spring transactions for production environments with connection pool tuning, monitoring strategies, and distributed transaction patterns.