What is the difference between REQUIRED and REQUIRES_NEW
4. Flush before REQUIRES_NEW if inner reads same data — em.flush() to avoid stale reads. 5. Use REQUIRED for atomically related operations — never split atomic logic across REQU...
🟢 Junior Level
REQUIRED and REQUIRES_NEW are the two most popular transaction propagation types in Spring. The main difference: REQUIRED uses an already existing transaction, while REQUIRES_NEW always creates a new one.
Simple analogy:
- REQUIRED — like working on one document with a colleague. You both edit the same file. If one decides to delete everything — both lose their work.
- REQUIRES_NEW — like two separate documents. Each works in their own file. If one ruins their document — the other is unaffected.
SQL example:
// REQUIRED (default)
@Transactional // propagation = REQUIRED
public void createOrder(Order order) {
orderRepo.save(order);
auditService.log(order); // REQUIRED → same commit
// If createOrder fails — BOTH order AND audit roll back
}
// REQUIRES_NEW
@Transactional
public void createOrder(Order order) {
orderRepo.save(order);
auditService.log(order); // REQUIRES_NEW → independent commit
// If createOrder fails — order rolls back, BUT audit already saved
}
Key differences:
| Characteristic | REQUIRED | REQUIRES_NEW |
|---|---|---|
| New transaction | Only if none exists | Always |
| Suspend current | No | Yes |
| Commit | One common at the end | Independent |
| Inner rollback | Rolls back entire tx | Only inner |
| Outer rollback | Rolls back inner | Does NOT affect inner |
When to use REQUIRED:
- Standard business logic (90% of cases)
- When all operations must be atomic
When to use REQUIRES_NEW:
- Auditing, logging
- Notifications that must save independently
- Operations that shouldn’t depend on the outer transaction
🟡 Middle Level
How it works internally
REQUIRED:
Call: serviceA.methodA() → proxy → TransactionInterceptor
1. Interceptor checks: is there an active transaction?
- Yes → joins, increment reference count
- No → creates new via TransactionManager
2. Method executes
3. On commit:
- If outer tx → commit
- If inner tx (join) → does nothing (commit only in outer)
4. On rollback:
- setRollbackOnly() → marks entire transaction for rollback
- Outer tx on commit sees rollback-only → UnexpectedRollingBackException
REQUIRES_NEW:
Call: serviceA.methodA() → proxy → innerService.methodB(REQUIRES_NEW)
1. Suspend current transaction:
- Connection unbind from ThreadLocal
- Transaction synchronizations suspended
- TransactionSynchronizationManager state reset
2. New physical transaction created:
- New connection from pool
- New BEGIN in DB
3. Method executes
4. On commit → commit inner transaction
5. On rollback → rollback inner transaction
6. Resume outer transaction:
- Connection re-bind to ThreadLocal
- Synchronizations resumed
Practical application
Scenario 1: REQUIRED for atomic business logic
@Service
public class OrderService {
@Autowired private PaymentService paymentService;
@Autowired private InventoryService inventoryService;
@Transactional // REQUIRED
public OrderResult placeOrder(Order order) {
// All operations must be atomic
inventoryService.reserve(order.getItems()); // REQUIRED — join
PaymentResult payment = paymentService.charge(order.getTotal()); // REQUIRED — join
if (!payment.isSuccess()) {
throw new PaymentFailedException();
// Rollback: BOTH reserve AND charge roll back — consistency
}
orderRepo.save(order);
return OrderResult.success();
}
}
Scenario 2: REQUIRES_NEW for auditing
@Service
public class PaymentService {
@Autowired private AuditService auditService;
@Transactional // REQUIRED
public PaymentResult processPayment(Payment payment) {
// Audit BEFORE payment — must save even on gateway failure
auditService.logPaymentAttempt(payment); // REQUIRES_NEW
try {
PaymentResult result = gateway.charge(payment);
auditService.logPaymentSuccess(payment); // REQUIRES_NEW
return result;
} catch (GatewayException e) {
auditService.logPaymentFailure(payment, e); // REQUIRES_NEW
throw e;
// Payment rolls back, but attempt audit already committed
}
}
}
@Service
public class AuditService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logPaymentAttempt(Payment payment) {
auditRepo.save(new AuditEntry(payment, "ATTEMPT"));
// Commit — independent of outer transaction
}
}
Scenario 3: REQUIRES_NEW for independent side-effects
@Service
public class NotificationService {
@Transactional(propagation = Propagation.REQUIRES_NEW, timeout = 5)
public void sendOrderConfirmation(Order order) {
// If email sending fails — order should still be saved
emailService.send(order.getEmail(), "Order Confirmed");
notificationRepo.save(new Notification(order, "EMAIL_SENT"));
// Commit — notification saved even if outer tx rolls back
}
}
Typical mistakes
| Mistake | Consequence | Solution |
|---|---|---|
| REQUIRED + inner exception | Entire transaction marked rollback-only | try-catch inside inner method or REQUIRES_NEW |
| REQUIRES_NEW for related data | Inner commit creates inconsistent data — IF inner transaction writes data logically dependent on outer (e.g., order_items without order). For independent data (audit) — this is correct behavior. | Use REQUIRED for logically related operations |
| REQUIRES_NEW without timeout | Hanging inner tx blocks outer tx | Always set timeout for REQUIRES_NEW |
| Assuming REQUIRES_NEW = atomicity with outer | Inner tx commits immediately, outer may rollback | Understand: REQUIRES_NEW = independent tx |
REQUIRED when called via this |
Propagation ignored | Use separate bean or self-injection |
Comparison: REQUIRED vs REQUIRES_NEW
| Characteristic | REQUIRED | REQUIRES_NEW |
|---|---|---|
| Physical transactions | 1 | 2+ |
| Outer tx suspend | No | Yes |
| Connection usage | 1 | 2 (suspend + new) |
| Independent commit | No | Yes |
| Rollback propagation | Entire chain | Only own tx |
| Outer rollback effect | Rolls back inner | Doesn’t affect inner |
| Throughput overhead | Minimal | +10-20% |
| Connection pool pressure | Low | High (x2 per call) |
| Deadlock risk | Low | Medium (2 connections) |
| Use case | Core business logic | Audit, logging, notifications |
When NOT to use REQUIRES_NEW
- Atomic business logic — if all operations must commit/rollback together
- High-load systems — suspend/resume overhead + connection pool pressure
- Microservices with eventual consistency — each service manages its own transaction
- When inner tx depends on outer tx data — inner commit may commit incomplete data
🔴 Senior Level
Internal Implementation: Suspend/Resume and Transaction Synchronization
Suspend Mechanics (REQUIRES_NEW)
// AbstractPlatformTransactionManager.suspend()
protected final SuspendedResourcesHolder suspend(@Nullable Object transaction) {
if (transaction != null) {
// 1. Suspend transaction synchronizations (Hibernate session, etc.)
List<TransactionSynchronization> suspendedSynchronizations =
doSuspendSynchronization();
// 2. Unbind connection from ThreadLocal
// Spring stores the JDBC connection in ThreadLocal via ConnectionHolder.
// unbindResource = detach connection from current thread
// bindResource = attach back
ConnectionHolder conHolder = (ConnectionHolder)
TransactionSynchronizationManager.unbindResource(obtainDataSource());
// 3. Reset TransactionSynchronizationManager state
boolean wasActive = TransactionSynchronizationManager.isActualTransactionActive();
TransactionSynchronizationManager.setActualTransactionActive(false);
IsolationLevel currentIsolation =
TransactionSynchronizationManager.getCurrentTransactionIsolationLevel();
TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(null);
TransactionSynchronizationManager.setReadOnly(false);
TransactionSynchronizationManager.setCurrentTransactionName(null);
return new SuspendedResourcesHolder(
conHolder, suspendedSynchronizations, wasActive, currentIsolation);
}
return null;
}
What gets suspended:
// doSuspendSynchronization() — suspends all registered synchronizations
// TransactionSynchronization callbacks:
interface TransactionSynchronization {
void suspend(); // Called on suspend
void resume(); // Called on resume
void beforeCommit(boolean readOnly);
void beforeCompletion();
void afterCommit();
void afterCompletion(int status);
}
// Registered synchronizations include:
// - Hibernate Session synchronization
// - JPA EntityManager synchronization
// - Custom synchronizations (registered by application code)
Resume Mechanics
// AbstractPlatformTransactionManager.resume()
protected final void resume(@Nullable Object transaction,
@Nullable SuspendedResourcesHolder resourcesHolder) {
if (resourcesHolder != null) {
Object suspendedResources = resourcesHolder.getSuspendedResources();
if (suspendedResources != null) {
// 1. Re-bind connection to ThreadLocal
TransactionSynchronizationManager.bindResource(
obtainDataSource(), resourcesHolder.getConnectionHolder());
}
// 2. Resume synchronizations
doResumeSynchronization(resourcesHolder.getSuspendedSynchronizations());
// 3. Restore state
TransactionSynchronizationManager.setActualTransactionActive(
resourcesHolder.wasActive());
TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(
resourcesHolder.getSuspendedIsolationLevel());
}
}
REQUIRED Join Logic
// handleExistingTransaction() — REQUIRED case
case PROPAGATION_REQUIRED:
// Join existing transaction
// NO NEW transaction created
// Reference count incremented
return prepareTransactionStatus(
definition, // same parameters
transaction, // same transaction object
false, // newTransaction = false
false, // newSynchronization = false
debugEnabled,
null); // no suspended resources
// Important: rollback propagation
// If inner method (REQUIRED) throws RuntimeException:
// 1. TransactionInterceptor catches exception
// 2. Calls: status.setRollbackOnly()
// 3. AbstractPlatformTransactionManager marks ENTIRE transaction
// 4. Outer method cannot undo this
Rollback-only flag propagation:
// AbstractPlatformTransactionManager
public final void rollback(TransactionStatus status) throws TransactionException {
if (status.isCompleted()) {
return; // Already completed
}
DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
if (defStatus.isNewTransaction()) {
// This is outer tx → do rollback
doRollback(defStatus);
} else {
// This is inner tx (join) → only mark rollback-only
defStatus.setRollbackOnly();
// Outer tx on commit will see this flag and rollback
}
}
Architectural Trade-offs
Approach A: REQUIRED
- ✅ Pros: Atomicity (all or nothing), simplicity, minimal overhead (1 connection), full ORM compatibility
- ❌ Cons: No partial failure tolerance, inner exception → full rollback, rollback-only propagation
- Suitable for: financial transactions, order processing, any operation requiring full atomicity
Approach B: REQUIRES_NEW
- ✅ Pros: Independent commit, partial failure handling, audit trail, isolation from outer tx failures
- ❌ Cons: Suspend/resume overhead (5-15ms), x2 connection usage, risk of data inconsistency (inner committed, outer rolled back)
- Suitable for: audit logging, notification dispatch, independent side-effects, compliance tracking
Approach C: REQUIRES_NEW + Compensation
@Transactional
public void processWithCompensation(Data data) {
try {
sideEffectService.doWork(data); // REQUIRES_NEW — commit
} catch (Exception e) {
// sideEffect already committed — need compensation
compensationService.undoSideEffect(data); // REQUIRES_NEW
throw e; // Outer tx rollback
}
}
- ✅ Pros: Best of both worlds — independence + compensation
- ❌ Cons: Complexity, need to implement undo for each side-effect
- Suitable for: saga patterns, distributed systems, eventual consistency
Edge Cases and Corner Cases
1. Rollback-only propagation (most common problem):
@Transactional
public void outerMethod() {
try {
innerService.innerMethod(); // REQUIRED
} catch (Exception e) {
// Caught exception...
}
// ...but transaction already marked rollback-only!
// On commit: UnexpectedRollingBackException
}
@Transactional
public void innerMethod() { // REQUIRED
repo.save(entity);
throw new RuntimeException("fail");
// TransactionInterceptor: status.setRollbackOnly()
}
Why this happens:
innerMethod() — REQUIRED, joins existing transaction
On exception:
→ TransactionInterceptor.completeTransactionAfterThrowing()
→ tm.rollback(txInfo.getTransactionStatus())
→ AbstractPlatformTransactionManager.rollback()
→ isNewTransaction() = false (join)
→ defStatus.setRollbackOnly() // NOT doRollback!
Outer method on commit:
→ tm.commit(status)
→ status.isRollbackOnly() = true
→ doRollback(status) // Forced rollback!
→ throw UnexpectedRollingBackException
Solutions:
// Solution A: REQUIRES_NEW
innerService.innerMethod(); // REQUIRES_NEW — rollback only own tx
// Solution B: try-catch inside inner method
@Transactional
public void innerMethod() {
try {
repo.save(entity);
throw new RuntimeException("fail");
} catch (Exception e) {
log.error("Inner failed, but won't rollback outer", e);
// Exception doesn't propagate → outer tx not marked rollback-only
}
}
// Solution C: noRollbackFor
@Transactional(noRollbackFor = BusinessException.class)
public void innerMethod() {
repo.save(entity);
throw new BusinessException("fail"); // Doesn't cause rollback
}
2. REQUIRES_NEW and connection pool exhaustion:
Load: 100 concurrent requests
Each request: 1 outer tx + 2 REQUIRES_NEW calls = 3 connections
Total connections needed: 100 × 3 = 300
HikariCP pool size: 50
Result:
- 50 requests get connections
- 250 wait for connectionAcquisitionTimeout (default 30 sec)
- Timeout → request failure
Solution:
spring:
datasource:
hikari:
maximum-pool-size: 150 # >= max_concurrent × max_nested_tx
connection-timeout: 5000 # faster failure
3. REQUIRES_NEW and Hibernate Session:
@Transactional
public void outerMethod() {
Entity outer = em.find(Entity.class, 1L); // loaded in Session 1
outer.setName("outer-change");
innerService.innerMethod(); // REQUIRES_NEW → suspend Session 1, create Session 2
// Session 2 does NOT see changes from Session 1 (not flushed)
// If innerMethod reads the same entity → sees old value
// On resume Session 1:
// Session 1 still contains "outer-change" in dirty state
// But changes not yet written to DB
}
Impact: Inner tx may read stale data. Solution: em.flush() before REQUIRES_NEW call.
4. Transaction Synchronization and REQUIRES_NEW:
// outer tx registers synchronization
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCommit() {
// Called after outer tx commit
}
@Override
public void afterCompletion(int status) {
// Called after outer tx completion
}
@Override
public void suspend() {
// Called on REQUIRES_NEW suspend
}
@Override
public void resume() {
// Called on REQUIRES_NEW resume
}
}
);
// inner tx (REQUIRES_NEW) has its own synchronizations
// inner afterCommit called after inner commit, NOT outer
5. REQUIRES_NEW and Isolation Level:
@Transactional
public void outerMethod() {
// outer tx uses default isolation (or set on outer)
innerService.innerMethod(); // REQUIRES_NEW
// inner tx can set its own isolation level
}
@Transactional(propagation = Propagation.REQUIRES_NEW,
isolation = Isolation.SERIALIZABLE)
public void innerMethod() {
// inner tx: SERIALIZABLE
// outer tx: DEFAULT (Read Committed)
// These isolation levels do NOT affect each other (different physical tx)
}
6. REQUIRES_NEW and Timeout:
@Transactional(propagation = Propagation.REQUIRES_NEW, timeout = 5)
public void innerMethod() {
// timeout = 5 seconds for inner tx ONLY
// outer tx timeout NOT affected
longRunningOperation(); // If > 5 sec → inner tx timeout
// outer tx continues (but inner already rolled back)
}
7. Nested REQUIRES_NEW cascade:
@Transactional
public void level0() {
level1Service.level1(); // REQUIRES_NEW → suspend L0, create L1
@Transactional(propagation = REQUIRES_NEW)
public void level1() {
level2Service.level2(); // REQUIRES_NEW → suspend L1, create L2
}
}
// Connection stack:
// L0: connection-1 (suspended)
// L1: connection-2 (suspended)
// L2: connection-3 (active)
//
// Commit order: L2 → resume L1 → L1 commit → resume L0 → L0 commit
// Rollback order: L2 rollback → resume L1 → L1 can continue or rollback
Performance Implications
| Metric | REQUIRED | REQUIRES_NEW |
|---|---|---|
| Latency (overhead) | ~0ms (baseline) | +5-15ms (suspend/resume) |
| Connection usage | 1 | 2+ (per nested call) |
| Throughput (single call) | 25,000 TPS | 20,000 TPS (-20%) |
| Throughput (5 nested) | 25,000 TPS | 10,000 TPS (-60%) |
| Connection pool pressure | 1x | Nx (N = nesting depth) |
| Hibernate Session sync overhead | None | Flush + clear per suspend/resume |
Figures provided for illustration of relative proportions (Spring Boot 3.2, PostgreSQL 15, HikariCP pool=50). Absolute values depend on DB, schema, hardware, and load. Key takeaway — REQUIRES_NEW adds 20-60% overhead per nesting level.
Scenario: 1 outer tx + 1 REQUIRES_NEW call
- Suspend: ~2ms
- New connection acquisition: ~0.5ms
- Inner tx execution: baseline
- Inner commit: ~1ms
- Resume: ~2ms
- Total overhead: ~5.5ms per REQUIRES_NEW call
Suspend includes: unbind Connection from ThreadLocal (~0.1ms), suspend Hibernate
Session and flush L1 cache (~1.5ms), unregister transaction synchronizations (~0.4ms).
Scenario: 1 outer tx + 3 REQUIRES_NEW calls
- Total overhead: ~16.5ms
- Throughput reduction: ~35%
Connection pool sizing:
- REQUIRED only: pool = max_concurrent_requests
- With 1 REQUIRES_NEW: pool = max_concurrent_requests × 2
- With N REQUIRES_NEW: pool = max_concurrent_requests × (N + 1)
Memory Implications
- Suspended ConnectionHolder: ~2KB (connection reference + state)
- Suspended TransactionSynchronizations: ~500 bytes – 5KB (depends on count)
- ThreadLocal state per suspend: ~1-2KB
- Hibernate Session state (suspended): ~100KB – 5MB (depends on L1 cache size)
- Inner tx EntityManager: ~100KB – 5MB (new PersistenceContext)
Total per REQUIRES_NEW call: ~200KB – 10MB (mostly Hibernate Session state)
Concurrency Aspects
REQUIRES_NEW concurrent execution:
Thread-1: outer(REQUIRED) → suspend → inner1(REQUIRES_NEW) → commit → resume → outer commit
Thread-2: outer(REQUIRED) → suspend → inner2(REQUIRES_NEW) → commit → resume → outer commit
inner1 and inner2 execute in parallel (different connections)
outer1 and outer2 wait on suspend phase until inner completes
Deadlock potential:
- Inner tx may lock rows needed by outer tx after resume
- Outer tx may lock rows needed by inner tx
→ Deadlock (detectable by DB deadlock detector)
Deadlock scenario:
T1-outer: UPDATE accounts SET balance = 900 WHERE id = 1; -- lock row 1
T1-inner(REQUIRES_NEW): UPDATE accounts SET balance = 800 WHERE id = 2; -- lock row 2
T2-inner(REQUIRES_NEW): UPDATE accounts SET balance = 700 WHERE id = 1; -- wait row 1
T2-outer: UPDATE accounts SET balance = 600 WHERE id = 2; -- wait row 2
→ Deadlock! T1-inner waits for T2-outer, T2-inner waits for T1-outer
Timeline: T=0: T1-outer locks row 1. T=1: T2-inner tries row 1 — BLOCKED.
T=2: T1-inner locks row 2. T=3: T2-outer tries row 2 — BLOCKED.
Deadlock detector: T2-outer → T1-inner → T2-inner → T1-outer → cycle.
Real Production Scenario
Situation: Healthcare data platform (2024), Spring Boot 3.1, MySQL 8.0, processing 5,000 patient records/hour.
Problem: When processing patient records, audit log was lost on processing failure. Regulatory audit found that 15% of attempted accesses were not logged.
Original code:
@Transactional
public void processPatientRecord(PatientRecord record) {
// Audit with REQUIRED — part of common transaction
auditService.logAccess(record); // REQUIRED
// Data processing — may fail
Data validated = validateAndTransform(record);
patientRepo.save(validated);
// If validateAndTransform fails → rollback EVERYTHING, including audit
// Audit record lost → compliance violation
}
Root cause: auditService.logAccess() used REQUIRED by default. On any processing failure, the audit record rolled back with the main transaction.
Solution:
@Transactional
public void processPatientRecord(PatientRecord record) {
// Audit BEFORE processing — REQUIRES_NEW
auditService.logAccess(record); // REQUIRES_NEW → commit immediately
try {
Data validated = validateAndTransform(record);
patientRepo.save(validated);
} catch (Exception e) {
// Record processing failed, but access audit preserved
throw e;
}
}
@Service
public class AuditService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logAccess(PatientRecord record) {
auditRepo.save(new AuditEntry(record, "ACCESS_ATTEMPT"));
// Commit — independent of outer transaction
}
}
Result:
- Audit coverage: from 85% to 100%
- Compliance: passed regulatory audit
- Throughput: -3% (REQUIRES_NEW overhead) — acceptable
- Connection pool: increased from 30 to 45
Monitoring and Diagnostics
Track REQUIRES_NEW usage:
@Aspect
@Component
public class TransactionPropagationMonitor {
private final MeterRegistry registry;
@Around("@annotation(transactional)")
public Object monitor(ProceedingJoinPoint pjp, Transactional transactional) throws Throwable {
String propagation = transactional.propagation().name();
Timer.Sample sample = Timer.start(registry);
try {
Object result = pjp.proceed();
sample.stop(Timer.builder("spring.transaction.execution")
.tag("propagation", propagation)
.tag("result", "success")
.register(registry));
return result;
} catch (Throwable ex) {
sample.stop(Timer.builder("spring.transaction.execution")
.tag("propagation", propagation)
.tag("result", "failure")
.register(registry));
throw ex;
}
}
}
Best Practices for Highload
- Minimize REQUIRES_NEW nesting — each level adds suspend/resume overhead + connection.
- Set timeout on REQUIRES_NEW — prevents hanging inner transactions.
- Size connection pool appropriately — pool = max_concurrent × (1 + max_nested_REQUIRES_NEW).
- Flush before REQUIRES_NEW if inner reads same data —
em.flush()to avoid stale reads. - Use REQUIRED for atomically related operations — never split atomic logic across REQUIRES_NEW.
- Audit always = REQUIRES_NEW — audit data must survive outer rollback.
🎯 Interview Cheat Sheet
Must know:
- REQUIRED uses existing transaction (or creates new), REQUIRES_NEW always creates new and suspends outer
- REQUIRED: 1 physical transaction, REQUIRES_NEW: 2+ physical transactions
- REQUIRED rollback-only propagation: inner RuntimeException marks entire tx for rollback
- REQUIRES_NEW: suspend/resume overhead (~5-15ms), uses 2 connections
- REQUIRES_NEW inner commit is independent — outer rollback doesn’t affect it
- Connection pool sizing: with REQUIRES_NEW, pool = max_concurrent × (1 + nesting_depth)
Common follow-up questions:
- What happens if inner REQUIRED throws and outer catches? — Transaction marked rollback-only → UnexpectedRollingBackException on commit
- Why REQUIRES_NEW needs 2 connections? — Outer connection suspended, new one created for inner tx
- When to use REQUIRES_NEW vs REQUIRED? — REQUIRES_NEW: audit/logging, independent side-effects; REQUIRED: atomic business logic
- How does Hibernate Session behave with REQUIRES_NEW? — Session suspended, new Session created — inner can’t see outer’s unflushed changes
Red flags (DO NOT say):
- “REQUIRES_NEW and REQUIRED are the same for inner calls” — REQUIRED joins, REQUIRES_NEW suspends+creates new
- “Outer rollback affects REQUIRES_NEW inner” — REQUIRES_NEW already committed, independent
- “REQUIRES_NEW doesn’t affect performance” — 20-60% overhead per nesting level
Related topics:
- [[13. What is Propagation in Spring]]
- [[14. What does Propagation.NESTED do]]
- [[16. What is @Transactional annotation]]
- [[22. What happens when calling @Transactional method from another method of the same class]]