What is the Difference Between REQUIRED and REQUIRES_NEW
When an inner method annotated with REQUIRED is invoked from an active transactional context: 4. When the inner method returns, commit() is not called—the decision to commit is...
🟢 Junior Level
REQUIRED and REQUIRES_NEW are the two foundational transaction propagation modes in Spring Framework. Their defining difference lies in whether an existing physical database transaction is reused or a new, independent physical transaction is created.
The Core Concept in 30 Seconds
REQUIRED(Default): “If a transaction already exists, I will join it. If none exists, I will open a new one.” All operations share a single database connection and are finalized by a single unifiedCOMMIT. If any method fails, the entire chain rolls back.REQUIRES_NEW: “I do not care about any surrounding transaction. I always demand my own independent transaction.” The active transaction is temporarily suspended, a second physical connection is checked out from the pool, and the work is committed immediately upon exiting the method. Even if the surrounding transaction later aborts, data committed byREQUIRES_NEWremains permanently saved in the database.
Execution Timeline: Order Creation with Audit Logging
Scenario: Order Creation with Audit Log
[ REQUIRED (Default) ]
createOrder() ------------[ START TRANSACTION ] (Connection #1)
├── saveOrder() │ (insert order)
├── auditLog() │ (insert audit log on the SAME connection)
└── throw Exception! │
--------------------------[ ROLLBACK ] (Both order AND audit log are rolled back!)
[ REQUIRES_NEW for Audit ]
createOrder() ------------[ START TX #1 ] (Connection #1)
├── saveOrder() │
├── auditLog() -------- │ ---[ START TX #2 ] (Connection #2, TX #1 suspended)
│ ├── saveAudit()
│ ---[ COMMIT TX #2 ] (Audit log is permanently saved!)
└── throw Exception! │
--------------------------[ ROLLBACK TX #1 ] (Order rolled back, but audit log remains!)
Comprehensive Comparison
| Characteristic | Propagation.REQUIRED |
Propagation.REQUIRES_NEW |
|---|---|---|
| New Transaction Creation | Only if no transaction exists in caller | Always (100% of invocations) |
| Physical DB Connections | 1 connection from pool | 2 concurrent connections per thread |
| Suspension of Current Tx | None | Yes (suspend outer, resume after inner completes) |
| Independent COMMIT | No (single unified commit at the end) | Yes (commits immediately on inner method exit) |
| Inner Exception Impact | Marks the entire chain as rollback-only |
Rolls back only the inner transaction |
| Outer Rollback Impact | Rolls back all inner and outer mutations | Has zero impact on already-committed REQUIRES_NEW |
| Typical Production Scenario | Core business logic, balance transfers, orders | Security audit trails, dead-letter logs, billing attempts |
Practical Spring Boot Example
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final SecurityAuditService auditService;
public OrderService(OrderRepository orderRepository, SecurityAuditService auditService) {
this.orderRepository = orderRepository;
this.auditService = auditService;
}
@Transactional // REQUIRED by default
public void registerOrder(Order order) {
orderRepository.save(order);
// Audit log must persist even if subsequent business logic fails:
auditService.logAttempt("Attempting creation of order #" + order.getId());
if (order.getItems().isEmpty()) {
// Throwing an exception rolls back the order insertion,
// but the audit record from logAttempt has ALREADY committed to the DB!
throw new IllegalArgumentException("Order items cannot be empty");
}
}
}
@Service
public class SecurityAuditService {
private final AuditRecordRepository auditRepository;
public SecurityAuditService(AuditRecordRepository auditRepository) {
this.auditRepository = auditRepository;
}
@Transactional(propagation = Propagation.REQUIRES_NEW, timeout = 3)
public void logAttempt(String message) {
auditRepository.save(new AuditRecord(message));
// Physical database COMMIT occurs at the instant this method finishes!
}
}
🟡 Middle Level
Spring Framework Under-the-Hood Mechanics
1. How REQUIRED Joins an Existing Transaction
When an inner method annotated with REQUIRED is invoked from an active transactional context:
TransactionInterceptorconsultsTransactionSynchronizationManagerand detects an activeConnectionHolder.- No new database transaction is created. Spring instantiates a
TransactionStatuswithisNewTransaction() == false. - All repository statements execute across the existing JDBC connection.
- When the inner method returns,
commit()is not called—the decision to commit is delegated to the outermost boundary (isNewTransaction() == true). - The Rollback-Only Trap: If the inner method throws an unhandled
RuntimeException, the interceptor marks the shared physical transaction asstatus.setRollbackOnly(). Even if the parent catches the error intry-catch, the outer commit will fail.
2. How REQUIRES_NEW Suspends and Opens a New Connection
- Spring unbinds the current connection from the thread:
TransactionSynchronizationManager.unbindResource(dataSource). - It suspends the active Hibernate session and transaction callbacks via
TransactionSynchronization.suspend(). - It packages the active state into a
SuspendedResourcesHolder. - It checks out a second physical connection from the HikariCP pool and executes
connection2.setAutoCommit(false). - It binds the new connection to the active thread’s
ThreadLocal. - It executes the inner method.
- Upon successful return, it immediately executes
connection2.commit()and closes/returns the second connection to the pool. - In a
finallyblock, Spring invokesresume(), restoring theSuspendedResourcesHolderand re-binding the original connection to the thread.
🔴 Senior Level
The Single-Thread Row Lock Self-Deadlock Hazard
A catastrophic mistake when using REQUIRES_NEW is attempting to modify the same row in both the outer and inner transactions:
The Single-Thread Self-Deadlock Scenario:
Tomcat Worker Thread T1:
1. outerMethod() [REQUIRED]:
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
──► Database places an exclusive row lock (X-lock) on row id = 1 in Connection #1.
2. outerMethod() invokes innerMethod() [REQUIRES_NEW]:
──► Spring suspends Connection #1.
──► Spring checks out Connection #2 in the SAME worker thread T1.
3. innerMethod() [REQUIRES_NEW] executes:
UPDATE accounts SET status = 'VIP' WHERE id = 1;
──► Connection #2 requests an X-lock on row id = 1 in the database.
──► The database detects that row id = 1 is already locked by Connection #1,
so it PAUSES Connection #2, waiting for the lock to release!
FATAL SINGLE-THREAD DEADLOCK:
• Connection #2 waits indefinitely for Connection #1 to commit and release the lock.
• Connection #1 can NEVER commit because its thread T1 is blocked waiting for Connection #2 to return!
• The thread hangs until lock_timeout (PostgreSQL) or innodb_lock_wait_timeout (MySQL) aborts the statement!
HikariCP Connection Pool Exhaustion Deadlock
Because REQUIRES_NEW holds the parent connection in suspended state while checking out a second connection:
HikariCP maximum-pool-size = 10
10 concurrent HTTP worker threads arrive simultaneously:
Each thread (1..10) executes outerMethod() [REQUIRED]:
──► All 10 threads acquire Connection #1 from the pool.
──► Available connections in HikariCP: 0!
Each thread (1..10) then executes innerMethod() [REQUIRES_NEW]:
──► All 10 threads simultaneously request a second connection from HikariCP.
──► The pool is empty! All 10 threads block in the HikariCP queue.
──► No thread can finish outerMethod() to return its first connection because
all threads are waiting for a second connection.
──► RESULT: Complete application deadlock until HikariCP throws:
SQLTransientConnectionException: Connection is not available, request timed out after 30000ms.
Production Architectural Countermeasures:
- Capacity Sizing: $\text{PoolSize} \ge \text{ConcurrentThreads} \times 2 + \text{SafetyMargin}$.
- Dedicated Audit Pool (Multi-DataSource): Route audit repositories with
REQUIRES_NEWto an isolated secondary connection pool. - Mandatory Timeouts: Always declare a short timeout:
@Transactional(propagation = Propagation.REQUIRES_NEW, timeout = 3). - Transition to Transactional Events (Decoupling): Replace synchronous
REQUIRES_NEWwith@TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)so the audit executes strictly after the parent connection has returned to the pool.
Hibernate L1 Cache Visibility Gap
Because REQUIRES_NEW executes in a completely separate physical database connection:
- If the outer transaction modifies an entity in memory, but Hibernate has not executed a
flush(), the inner method cannot see those mutations. - Even after
entityManager.flush(), the inner method running underRead Committedcannot read uncommitted data written by the outer connection (Dirty Read prevention).findById()in the inner method will return the pre-transaction database state. - Conversely, mutations committed by the inner method do not automatically reflect in the outer transaction’s Hibernate L1 cache, leading to potential stale reads in the parent method.
4 Tricky Questions
1. What happens if a method annotated with REQUIRES_NEW attempts to UPDATE the same row updated by the outer REQUIRED method?
Answer: The thread hangs and ultimately crashes with a database lock wait timeout exception:
- In PostgreSQL:
ERROR: canceling statement due to lock timeout. - In MySQL InnoDB:
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction. Underlying Cause: The outer transaction acquired an exclusive row lock (X-lock) on the row and was suspended. The innerREQUIRES_NEWtransaction runs on a separate physical database connection on the same JVM thread and requests anX-lockon that identical row. Because Connection #2 waits for Connection #1 to release the lock, and Connection #1 cannot resume until Connection #2 finishes, the thread deadlocks against itself.
2. Why does catching an exception from REQUIRES_NEW allow the outer transaction to commit, whereas catching an exception from REQUIRED causes an UnexpectedRollbackException?
Answer:
- Under
REQUIRED: Both methods share a singleTransactionStatus. When an exception escapes the inner method, the innerTransactionInterceptorunconditionally marks the shared status assetRollbackOnly(). When the parent catches the exception and attempts to commit, Spring inspects therollback-onlyflag, aborts the commit, and throwsUnexpectedRollbackException. - Under
REQUIRES_NEW: The inner method has its own independentTransactionStatus(isNewTransaction() == true). When it throws an exception, the interceptor rolls back only that inner physical transaction. The parent transaction’s status remains clean. The parent method can catch the exception intry-catchand commit its own work successfully.
3. Can a method with Propagation.REQUIRES_NEW read an entity saved via repository.save() in the parent REQUIRED method immediately prior to the call?
Answer: No, by default it cannot! Two barriers prevent this:
- Delayed Flush in Hibernate: Calling
repository.save()simply associates the entity with the currentPersistenceContextin JVM memory. UnlessentityManager.flush()is explicitly invoked, no SQLINSERTis sent to the database. - Transaction Isolation: Even if
flush()is executed, the newly inserted row is uncommitted inside Connection #1. Because Connection #2 (REQUIRES_NEW) operates underRead Committed(or higher), it cannot read uncommitted data from Connection #1.findById()in the inner method will returnOptional.empty().
4. How can synchronous audit logging with REQUIRES_NEW be refactored using Spring Events to eliminate connection pool deadlocks?
Answer:
By replacing direct method calls with Spring’s @TransactionalEventListener configured with TransactionPhase.AFTER_COMPLETION:
// Inside the business service:
@Transactional
public void processOrder(Order order) {
orderRepository.save(order);
// Publish a domain event instead of calling an audit service directly:
eventPublisher.publishEvent(new OrderProcessedEvent(order.getId()));
}
// Inside the audit listener component:
@Component
public class AuditEventListener {
private final AuditRepository auditRepository;
public AuditEventListener(AuditRepository auditRepository) {
this.auditRepository = auditRepository;
}
// Fires strictly AFTER the outer transaction has committed or rolled back,
// when Connection #1 has ALREADY been released back to the HikariCP pool!
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void onOrderProcessed(OrderProcessedEvent event) {
auditRepository.save(new AuditLog("Order #" + event.orderId() + " processed"));
}
}
This guarantees that worker threads never hold two connection pool connections at the same time.
🎯 Interview Cheat Sheet
- Core Difference:
REQUIREDjoins an existing transaction (1 connection, unified commit);REQUIRES_NEWalways creates a new physical transaction (2 concurrent connections, independent commit). - Rollback Behavior:
REQUIRED: Inner failure marks the entire transactionrollback-only(UnexpectedRollbackExceptionon outer commit).REQUIRES_NEW: Inner failure rolls back only the inner transaction; outer method can catch and commit cleanly.
- Connection Pool Exhaustion:
REQUIRES_NEWchecks out 2 connections per thread simultaneously; insufficient pool sizing causes complete JVM connection deadlocks. - Self-Deadlock Hazard: Updating the same row in
REQUIREDand an innerREQUIRES_NEWcauses an unresolvable single-thread lock timeout. - Best Practice for Audit: Decouple audit writes using
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)instead of synchronousREQUIRES_NEW.