Back to All Posts
Spring Framework

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

March 4, 2026
12 min read
Abid Hasan
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:

PropertyWhat It MeansReal-World Analogy
AtomicityAll operations succeed or all failThe restaurant order: either you get everything, or you get nothing
ConsistencyDatabase moves from one valid state to anotherYour bank account balance is always correct
IsolationConcurrent transactions don't interfereTwo people booking the same seat—one gets it, the other doesn't
DurabilityCommitted data survives failuresOnce 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:

  1. Annotation Detection: Spring scans for @Transactional annotations during component scanning
  2. Proxy Creation Decision: AbstractAutoProxyCreator determines which beans need proxies based on advices and advisors
  3. Proxy Generation: Either JDK dynamic proxy or CGLib proxy is created
  4. 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.Proxy class
  • Method calls are dispatched through InvocationHandler using 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 final classes 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

AspectJDK Dynamic ProxyCGLib Proxy
MechanismInterface-basedSubclass-based
RequirementMust implement interfaceNo interface needed
ConstructorAny constructorNeeds default/no-arg constructor
Final methodsCan proxy (if in interface)Cannot proxy final methods/classes
Proxy CreationFast (uses reflection)Slower (bytecode generation)
Method InvocationSlower (reflection-based)Faster (direct calls)
Spring Boot 2.x+ DefaultPreferred when interface existsFallback 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:

  1. Before Each Advisor: Each advisor's "before" logic runs
  2. Target Method: The actual business method executes
  3. 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:

  1. Creates a transaction before method execution (using PlatformTransactionManager)
  2. Binds the transaction to the current thread
  3. Executes your business method
  4. 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:

ManagerUse Case
DataSourceTransactionManagerFor JDBC
JpaTransactionManagerFor JPA/Hibernate
JtaTransactionManagerFor 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.

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:

  1. Spring Proxy intercepts the call
  2. TransactionInterceptor creates a new transaction
  3. PlatformTransactionManager begins the database transaction
  4. Your method executes the business logic
  5. If successful → Commit
  6. 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 @Transactional parameters 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 @Transactional methods
  • 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 final classes or methods—a common gotcha
  • Multiple advisors chain together for cross-cutting concerns (security, caching, logging)
  • The TransactionInterceptor manages the complete transaction lifecycle
  • PlatformTransactionManager abstracts different transaction APIs
  • Prefer declarative transactions for cleaner, more maintainable code
  • Understanding proxy types helps debug transaction issues (e.g., self-invocation problems)

Tags

#Spring#Transactions#Java#Backend#Database