What is @Transactional annotation?
@Transactional is a Spring annotation that allows you to manage transactions declaratively. Instead of manual BEGIN/COMMIT/ROLLBACK, you just add the annotation — Spring does ev...
🟢 Junior Level
@Transactional is a Spring annotation that allows you to manage transactions declaratively. Instead of manual BEGIN/COMMIT/ROLLBACK, you just add the annotation — Spring does everything automatically.
Simplest example
@Service
public class UserService {
@Transactional
public void createUser(User user) {
userRepository.save(user);
// Spring automatically does COMMIT on success
// or ROLLBACK on RuntimeException
}
}
Analogy
Think of a bank transfer: you either transfer the full amount or not at all. @Transactional guarantees that half a transfer never “hangs” in the database.
Key parameters
| Parameter | What it does | Default value |
|---|---|---|
propagation |
How transaction relates to others | REQUIRED |
isolation |
Isolation level | DEFAULT (DB level) |
readOnly |
Read-only optimization | false |
rollbackFor |
Which exceptions trigger rollback | RuntimeException, Error |
timeout |
Timeout in seconds | -1 (no timeout) |
When to use
Any method that performs multiple DB operations that must be atomic: money transfers, creating orders with items, updating related entities.
Important rules
- Works only on public methods
- Works only when called from outside the class (not via
this) - By default rolls back only on RuntimeException and Error
🟡 Middle Level
How it works: AOP Proxy mechanism
External call → Proxy → TransactionInterceptor → PlatformTransactionManager
↓
Open transaction → Execute method → COMMIT/ROLLBACK
- Spring creates a Proxy object around the bean (JDK Dynamic Proxy or CGLIB)
- On method call, control goes to
TransactionInterceptor - Interceptor consults
PlatformTransactionManager, which opens connection and starts transaction - Your method executes
- On success —
commit, on exception —rollback
Where you can place the annotation
| Level | Works? | Recommendation |
|---|---|---|
| Method | ✅ Yes | Best option — precise control |
| Class | ✅ Yes | All public methods are transactional |
| Interface | ⚠️ Depends | Only with JDK Dynamic Proxy |
| Private method | ❌ No | Proxy doesn’t intercept |
Example with parameters
@Transactional(
propagation = Propagation.REQUIRED,
isolation = Isolation.READ_COMMITTED,
timeout = 30,
readOnly = false,
rollbackFor = Exception.class,
noRollbackFor = NotFoundException.class
)
public void processOrder(Order order) { ... }
Common mistakes
| Mistake | What happens | How to fix |
|---|---|---|
@Transactional on private method |
Annotation ignored | Make method public |
Calling this.method() inside class |
Proxy bypassed, no transaction | Extract to separate bean or self-injection |
Checked Exception without rollbackFor |
COMMIT instead of ROLLBACK | Add rollbackFor = Exception.class |
@Transactional on Controller |
DB connection held during JSON serialization | Move to Service layer |
| Catching and swallowing exception | Spring doesn’t see exception → COMMIT | Rethrow exception or call setRollbackOnly() |
When NOT to use
| Situation | Why not needed | Alternative |
|---|---|---|
| Method only reads one record | Excessive complexity | readOnly = true or no transaction |
| HTTP call inside method | Holds DB connection unnecessarily | Move HTTP call outside transaction |
| Logging/auditing | Doesn’t require atomicity with logic | REQUIRES_NEW in separate bean |
| Background message processing | May need different strategy | Programmatic transactions |
Comparison: declarative vs programmatic approach
| Approach | Pros | Cons | When to use |
|---|---|---|---|
@Transactional |
Clean code, declarative | Limited by proxy | Standard scenarios |
TransactionTemplate |
Full control, works inside this |
More boilerplate | Conditional transaction logic |
🔴 Senior Level
Spring Transaction AOP — Internal Architecture
Proxy Creation Flow
// InfrastructureAdvisorAutoProxyCreator (BeanPostProcessor)
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
if (isInfrastructureClass(bean.getClass())) return bean;
if (AnnotationUtils.findAnnotation(bean.getClass(), Transactional.class) != null
|| hasTransactionalMethods(bean.getClass())) {
ProxyFactory proxyFactory = new ProxyFactory(bean);
proxyFactory.addAdvice(new TransactionInterceptor(tm, transactionAttributeSource));
return proxyFactory.getProxy();
}
return bean;
}
TransactionInterceptor Execution
public Object invoke(MethodInvocation invocation) throws Throwable {
Class<?> targetClass = AopProxyUtils.ultimateTargetClass(invocation.getThis());
TransactionAttribute txAttr = tas.getTransactionAttribute(
invocation.getMethod(), targetClass);
PlatformTransactionManager tm = determineTransactionManager(txAttr);
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, invocation.getMethod().toString());
Object retVal;
try {
retVal = invocation.proceed();
} catch (Throwable ex) {
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
} finally {
cleanupTransactionInfo(txInfo);
}
commitTransactionAfterReturning(txInfo);
return retVal;
}
JDK Dynamic Proxy vs CGLIB
| Characteristic | JDK Dynamic Proxy | CGLIB |
|---|---|---|
| Mechanism | Implements interfaces | Extends class |
| Creation speed | Faster (~5-10μs) | Slower (~20-50μs) |
| Call overhead | Low | Slightly higher |
@Transactional on interface |
✅ Works | ❌ Ignored |
| Final methods | ✅ Not a problem | ❌ Cannot override |
| Memory | Less | More (generates class) |
// JDK Dynamic Proxy
UserService proxy = (UserService) Proxy.newProxyInstance(
classLoader,
new Class[]{UserService.class},
handler
);
// CGLIB
UserService proxy = (UserService) Enhancer.create(UserService.class, handler);
Spring Boot defaults to CGLIB if there are no interfaces. Can force: @EnableTransactionManagement(proxyTargetClass = true).
Thread-Local Transaction State
public abstract class TransactionSynchronizationManager {
private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations = new NamedThreadLocal<>("Transaction synchronizations");
private static final ThreadLocal<String> currentTransactionName = new NamedThreadLocal<>("Current transaction name");
private static final ThreadLocal<Boolean> currentTransactionReadOnly = new NamedThreadLocal<>("Current transaction read-only status");
private static final ThreadLocal<Integer> currentTransactionIsolationLevel = new NamedThreadLocal<>("Current transaction isolation level");
private static final ThreadLocal<Boolean> actualTransactionActive = new NamedThreadLocal<>("Actual transaction active");
}
This is why @Transactional doesn’t work across threads in standard configuration — ThreadLocal is not transferred. @Async methods get a separate transaction context. When using custom TaskExecutor with ThreadLocal propagation or Virtual Threads (Java 21+), context propagation is possible but requires explicit configuration.
Edge Cases (minimum 3)
-
UnexpectedRollbackException: In a REQUIRED → REQUIRED chain, inner method throws RuntimeException. Outer catches it, but TransactionInterceptor already set
rollback-onlyflag. On commit attempt —UnexpectedRollbackExceptionis thrown. -
@Transactional on final method: CGLIB cannot override final method → annotation silently ignored. JDK Proxy also doesn’t intercept final interface methods.
-
Inheritance and @Transactional: If subclass overrides method without re-adding the annotation — behavior depends on proxy type. With CGLIB, superclass annotation may not be discovered.
-
Multiple TransactionManager: If application has two
PlatformTransactionManager,@Transactionalwithout qualifier uses@Primary. Calling two different methods with different managers = two independent transactions, not a distributed transaction. Need JTA/Atomikos for 2PC. JTA (Java Transaction API) and Atomikos (JTA implementation). 2PC (Two-Phase Commit) — protocol for atomic commit across multiple resources.
Performance Numbers
| Operation | Time |
|---|---|
| Proxy invocation overhead | ~1-2 μs |
| Transaction setup (Spring side) | ~5-10 μs |
| Connection acquisition (HikariCP) | ~50-200 μs |
| Typical DB query | 1-10 ms |
Spring overhead is negligible. DB operations dominate.
Memory Implications
Each active transaction holds:
- JDBC Connection from pool (~2-4 KB)
- Hibernate L1 cache (Persistence Context) — grows with number of loaded entities
- ThreadLocal entries in
TransactionSynchronizationManager - Undo log in DB (longer transaction = larger)
Long transactions = connection pool exhaustion + bloat in DB.
Thread Safety
@Transactional is not thread-safe by itself. Each thread gets:
- Separate transaction
- Separate DB connection
- Separate Hibernate Session
The service bean itself is a singleton. But transaction state is stored in ThreadLocal, so concurrent calls to the same @Transactional method are isolated.
Production War Story
In production, a service with @Transactional caught a checked BusinessException. Developer didn’t specify rollbackFor. Transaction did COMMIT, but part of data was already written. Client got an error, but half the order remained in the database. Solution: rollbackFor = Exception.class on all write methods + integration tests for rollback behavior.
Why Spring committed: by default, rollback only for RuntimeException + Error. Checked exception (inherits from Exception, not RuntimeException) is treated by TransactionInterceptor as “normal completion” → commit. Even though the method threw an exception, Spring doesn’t see it as a rollback signal — because the exception was caught in a catch block, and rollbackFor was not specified.
Monitoring
Spring Boot Debug Logging:
logging:
level:
org.springframework.transaction: DEBUG
org.springframework.orm.jpa: DEBUG
Actuator Metrics:
@Bean
public TransactionManagerCustomizers transactionManagerCustomizers(MeterRegistry registry) {
return new TransactionManagerCustomizers(tm -> {
// Custom metrics
});
}
Micrometer:
spring.datasource.connections.active
spring.datasource.connections.idle
Highload Best Practices
- Minimal transaction scope: Only DB operations. HTTP calls, file I/O — outside.
- readOnly for reads: Disables dirty checking (Hibernate compares entities with snapshot on flush; with readOnly = true this mechanism is disabled). Can route to read replica.
- Connection pool tuning: HikariCP
maximumPoolSize=CPU cores * 2 + disk spindles(rule-of-thumb from HikariCP creator. For SSD, “disk spindles” = 1. Starting point — 10-20, then tune under load). - Avoid long transactions: Split batch operations into chunks.
- Use REQUIRES_NEW for side-effects: Logging, notifications — shouldn’t block main transaction.
- Consider programmatic transactions: For complex conditional logic,
TransactionTemplateis more flexible.
🎯 Interview Cheat Sheet
Must know:
- @Transactional — declarative transaction management via AOP Proxy (JDK Dynamic Proxy or CGLIB)
- Works only on public methods and only when called from outside the class (not via
this) - By default rollback only for RuntimeException and Error, checked exceptions → commit
- Parameters: propagation (REQUIRED), isolation (DEFAULT), readOnly (false), timeout (-1), rollbackFor
- State stored in ThreadLocal — doesn’t work across threads (@Async = separate context)
- @Transactional on interface works only with JDK Dynamic Proxy, ignored with CGLIB
Common follow-up questions:
- Why doesn’t @Transactional work when called via this? — Proxy bypassed, TransactionInterceptor not involved
- JDK Proxy vs CGLIB? — JDK: interface-based, CGLIB: subclass-based (final methods not intercepted)
- What happens with @Transactional on Controller? — DB connection held during JSON serialization — anti-pattern
- How does UnexpectedRollbackException work? — Inner REQUIRED set rollback-only, outer tries to commit
Red flags (DO NOT say):
- “@Transactional on private method works” — proxy doesn’t intercept private methods
- “Checked exception causes rollback” — Spring commits by default
- “Can put on Controller” — connection held during JSON serialization, pool exhaustion
Related topics:
- [[13. What is Propagation in Spring]]
- [[17. At what level can you use @Transactional]]
- [[19. Which exceptions cause rollback by default]]
- [[22. What happens when calling @Transactional method from another method of the same class]]