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.
| Propagation | Existing transaction? | No existing transaction? |
|---|---|---|
| REQUIRED | Joins it | Creates a new one |
| REQUIRES_NEW | Suspends it, creates a new one | Creates a new one |
| NESTED | Creates a savepoint within it | Creates a new one (like REQUIRED) |
| SUPPORTS | Joins it | Runs non-transactionally |
| NOT_SUPPORTED | Suspends it, runs non-transactionally | Runs non-transactionally |
| MANDATORY | Joins it | Throws IllegalTransactionStateException |
| NEVER | Throws IllegalTransactionStateException | Runs 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
| Isolation | Dirty read | Non-repeatable read | Phantom read |
|---|---|---|---|
| READ_UNCOMMITTED | Possible | Possible | Possible |
| READ_COMMITTED | Prevented | Possible | Possible |
| REPEATABLE_READ | Prevented | Prevented | Possible |
| SERIALIZABLE | Prevented | Prevented | Prevented |
@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
@Transactionalmethod: commits (unlessrollbackForis set) - —
REQUIRES_NEWcalled viathis.method(): self-invocation — propagation is ignored, joins caller's transaction - —
NESTEDrollback: only rolls back to savepoint, outer transaction can continue - —
REQUIRES_NEWcommits then outer throws: inner changes are permanent - —
MANDATORYwith no active transaction: throwsIllegalTransactionStateException - —
@Transactionalon 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
