Question 15 · Section 11

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...

Language versions: English Russian Ukrainian

🟢 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

  1. Minimize REQUIRES_NEW nesting — each level adds suspend/resume overhead + connection.
  2. Set timeout on REQUIRES_NEW — prevents hanging inner transactions.
  3. Size connection pool appropriately — pool = max_concurrent × (1 + max_nested_REQUIRES_NEW).
  4. Flush before REQUIRES_NEW if inner reads same dataem.flush() to avoid stale reads.
  5. Use REQUIRED for atomically related operations — never split atomic logic across REQUIRES_NEW.
  6. 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]]