2V0-72.22 Exam Guide

Spring Transaction Propagation on the 2V0-72.22

Transaction propagation is one of the highest-yield topics on the exam. The questions are rarely about knowing the definition of REQUIRED — they give you two methods calling each other and ask what happens to each transaction when an exception is thrown at a specific point. You need to trace the transaction boundaries precisely.

The seven propagation types

Know all seven, but focus your energy on the first three — they appear in almost every transaction question.

PropagationExisting transaction?No existing transaction?
REQUIREDJoins itCreates a new one
REQUIRES_NEWSuspends it, creates a new oneCreates a new one
NESTEDCreates a savepoint within itCreates a new one (like REQUIRED)
SUPPORTSJoins itRuns non-transactionally
NOT_SUPPORTEDSuspends it, runs non-transactionallyRuns non-transactionally
MANDATORYJoins itThrows IllegalTransactionStateException
NEVERThrows IllegalTransactionStateExceptionRuns non-transactionally

REQUIRED vs REQUIRES_NEW — the classic exam scenario

This is the most tested configuration. Method A runs in a REQUIRED transaction and calls method B which uses REQUIRES_NEW. They run in completely separate transactions. What happens when each one throws?

@Service
public class OrderService {

    @Autowired
    private AuditService auditService;

    @Transactional  // REQUIRED by default
    public void placeOrder(Order order) {
        // runs in Transaction A
        orderRepo.save(order);

        // B runs in its own Transaction B (A is suspended)
        // If B commits and then A throws → B's changes are permanent
        // If A commits and B throws (and A catches it) → A can still commit
        auditService.logOrder(order);

        // If this line throws, Transaction A rolls back.
        // But Transaction B already committed — audit log stays.
        validateStock(order);
    }
}

@Service
public class AuditService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void logOrder(Order order) {
        // runs in Transaction B — independent of Transaction A
        auditRepo.save(new AuditEntry(order));
    }
}

The exam will describe this scenario and ask: if placeOrder throws after logOrder returns, is the audit entry saved? The answer is yes — Transaction B already committed independently.

NESTED vs REQUIRES_NEW — not the same thing

This distinction appears less often but is worth knowing. NESTED uses a savepoint inside the same physical transaction. If the nested part rolls back, only the work since the savepoint is undone — the outer transaction can still commit. But if the outer transaction rolls back, everything rolls back, including the nested part.

// REQUIRES_NEW: completely separate transaction
// → outer rollback does NOT affect inner (already committed)
// → inner rollback does NOT affect outer

// NESTED: savepoint within the same transaction
// → inner rollback reverts to savepoint, outer can continue
// → outer rollback undoes everything, including the nested part

@Transactional(propagation = Propagation.NESTED)
public void nestedOperation() {
    // If this throws and the caller catches it,
    // only this method's work is rolled back (to savepoint).
    // The outer transaction continues.
}

Note: NESTED requires JDBC savepoint support. Not all databases or transaction managers support it. The exam expects you to know the semantic difference from REQUIRES_NEW.

Rollback rules — checked exceptions do not roll back by default

Spring only rolls back a transaction automatically for RuntimeException and Error. Checked exceptions let the transaction commit unless you configure otherwise. This surprises most developers and the exam tests it directly.

// Checked exception — transaction COMMITS (default behavior)
@Transactional
public void processPayment() throws PaymentException {
    paymentRepo.save(payment);
    throw new PaymentException("Card declined"); // checked — no rollback
}

// RuntimeException — transaction ROLLS BACK (default behavior)
@Transactional
public void processPayment() {
    paymentRepo.save(payment);
    throw new IllegalStateException("Card declined"); // unchecked — rolls back
}

// Override: roll back on a specific checked exception
@Transactional(rollbackFor = PaymentException.class)
public void processPayment() throws PaymentException {
    paymentRepo.save(payment);
    throw new PaymentException("Card declined"); // now rolls back
}

// Override: do NOT roll back on a specific runtime exception
@Transactional(noRollbackFor = OptimisticLockingFailureException.class)
public void updateRecord() {
    // OptimisticLockingFailureException will not trigger rollback
}

The self-invocation trap with propagation

The same proxy-based limitation from AOP applies here. If a method calls another method on this, the call bypasses the proxy — the @Transactional annotation on the called method is completely ignored.

@Service
public class ReportService {

    @Transactional  // REQUIRED
    public void generateReport() {
        // This calls the real method directly — NOT through the proxy.
        // REQUIRES_NEW is ignored. archiveReport() joins the existing
        // transaction instead of creating a new one.
        this.archiveReport();
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void archiveReport() {
        // Intended to run in its own transaction.
        // But called via this.archiveReport() — runs in caller's transaction.
    }
}

The exam will show code exactly like this and ask what propagation behavior actually occurs. The answer is: REQUIRES_NEW has no effect — archiveReportparticipates in the caller's transaction.

Isolation levels — know which problem each one solves

IsolationDirty readNon-repeatable readPhantom read
READ_UNCOMMITTEDPossiblePossiblePossible
READ_COMMITTEDPreventedPossiblePossible
REPEATABLE_READPreventedPreventedPossible
SERIALIZABLEPreventedPreventedPrevented
@Transactional(isolation = Isolation.REPEATABLE_READ)
public BigDecimal getAccountBalance(Long accountId) {
    // Any read within this transaction will return the same value,
    // even if another transaction commits a change in between.
}

Other attributes worth knowing

// readOnly: hint to the persistence provider — can enable optimizations.
// Does NOT prevent writes at the Spring level.
@Transactional(readOnly = true)
public List<Order> findAll() { ... }

// timeout: rolls back if the transaction runs longer than N seconds
@Transactional(timeout = 30)
public void longRunningOperation() { ... }

// @Transactional on a class applies to all public methods in that class
@Transactional(readOnly = true)
@Service
public class ProductService {

    // Inherits readOnly = true from the class-level annotation
    public Product findById(Long id) { ... }

    // Overrides: this method gets its own transaction with readOnly = false
    @Transactional
    public Product save(Product product) { ... }
}

Class-level @Transactional is a common exam pattern. A method-level annotation always overrides the class-level one. Also: @Transactional on a private method is silently ignored — same proxy limitation as AOP.

Quick reference: what the exam actually asks

  • — Checked exception in @Transactional method: commits (unless rollbackFor is set)
  • REQUIRES_NEW called via this.method(): self-invocation — propagation is ignored, joins caller's transaction
  • NESTED rollback: only rolls back to savepoint, outer transaction can continue
  • REQUIRES_NEW commits then outer throws: inner changes are permanent
  • MANDATORY with no active transaction: throws IllegalTransactionStateException
  • @Transactional on a private method: silently ignored
  • — Class-level vs method-level annotation: method-level wins

Practice transaction questions under exam conditions

PrepForge mock exams include propagation and rollback scenarios with full explanations.

Start a Mock Exam