skip to content

When would you use MANDATORY propagation, and what happens if the method is called outside a transaction?

level: middleimportance: should knowfreq 55%

answer

  1. MANDATORY = join or fail
  2. throws IllegalTransactionStateException if none
  3. contract enforcement for low-level helpers
  4. never creates a tx (unlike REQUIRED)
  5. self-invocation bypasses the check

basics

~10 s

Use MANDATORY when a method must only run inside an existing transaction opened by its caller. If called with no active transaction, Spring throws IllegalTransactionStateException and the method body never runs.

solid answer

~40 s

MANDATORY declares that a method participates in a transaction but refuses to create one — it enforces that some caller up the stack already opened a transactional boundary. This is useful for low-level helpers (e.g. a ledger-append or audit-write) that must always be part of a larger atomic unit and would be a bug if run standalone. At runtime the transaction interceptor checks the thread-bound transaction state: if a transaction is active the method joins it; if not, Spring throws org.springframework.transaction.IllegalTransactionStateException ('No existing transaction found...') before executing the body. The important caveat is that the check only works when the call goes through the Spring proxy — a self-invocation within the same bean bypasses it, so the enforcement silently doesn't happen. It is a design/contract-enforcement tool, not a way to obtain a transaction.

code

java · 24 lines
java
@Service
public class LedgerService {

    // Only valid inside an existing transaction; refuses to run standalone.
    @Transactional(propagation = Propagation.MANDATORY)
    public void append(LedgerEntry entry) {
        ledgerRepo.save(entry);
    }
}

@Service
public class PaymentService {
    private final LedgerService ledger;

    PaymentService(LedgerService ledger) { this.ledger = ledger; }

    @Transactional // REQUIRED: opens the boundary the ledger demands
    public void pay(Payment p) {
        // ... business changes ...
        ledger.append(p.toLedgerEntry()); // joins THIS transaction
    }
}
// Calling ledger.append(...) directly, with no active tx,
// throws IllegalTransactionStateException before append() runs.

go deeper

for a junior

Know that MANDATORY needs an existing transaction and throws otherwise.

for a middle

Give a concrete use case (helper that must join the caller's tx) and the exact exception.

for a senior

Explain fail-fast contract enforcement vs REQUIRED's silent creation and the proxy self-invocation caveat.

for a principal

Discuss using MANDATORY to codify unit-of-work boundaries and catch missing-transaction defects at build/test time.

**MANDATORY** = `@Transactional(propagation = Propagation.MANDATORY)`. Its single job is to **assert** that the current thread already has an active transaction and then **join** it. It never starts, suspends, or creates a transaction. **Behavior matrix:** - Transaction active when method entered → method joins the existing physical transaction (same connection, same commit/rollback fate). - No transaction active → the interceptor throws `org.springframework.transaction.IllegalTransactionStateException` with message *"No existing transaction found for transaction marked with propagation 'mandatory'"*. Your method body does **not** run. **Why/when to use it:** MANDATORY is a **contract-enforcement** mechanism. In a layered architecture you sometimes have a fine-grained operation that is only ever meaningful as part of a broader unit of work — for example: appending to a financial ledger, writing an outbox event that must commit with the business change, or a repository-level mutation that must never auto-commit on its own. Annotating it MANDATORY turns 'someone forgot to wrap this in a transaction' from a silent data-integrity bug into a loud, fail-fast exception during development/testing. **Contrast with REQUIRED:** REQUIRED (default) would quietly open a new transaction if none exists — hiding the caller's mistake. MANDATORY refuses that convenience on purpose. If you want 'join or create,' use REQUIRED; if you want 'join or fail,' use MANDATORY. **Mechanism:** The check is performed by the AOP proxy's `TransactionInterceptor` using `TransactionSynchronizationManager`'s thread-bound state. Consequences: - **Self-invocation caveat:** `this.mandatoryMethod()` from another method of the same bean bypasses the proxy, so no propagation logic runs at all — the annotation is effectively ignored. Call through an injected reference / separate bean, or use AopContext.currentProxy(). - Works with any `PlatformTransactionManager` (JPA, JDBC, etc.). **Common gotchas:** - Developers expect MANDATORY to 'make sure there's a transaction' by creating one — it does not; it validates. - The exception is a `RuntimeException` thrown before the body, so wrapping the method's own logic in try/catch won't suppress it. - Testing: a unit test that calls the method without a surrounding transaction will fail with IllegalTransactionStateException, which is the intended, useful signal.

  • Why choose MANDATORY over REQUIRED for such a helper?
    REQUIRED silently opens its own transaction if none exists, hiding a caller bug; MANDATORY fails fast, guaranteeing the helper always commits as part of the caller's atomic unit.

saying these in an interview costs you the question

  • Claiming MANDATORY creates or guarantees a transaction by opening one
  • Not knowing the specific exception type
  • Forgetting that self-invocation makes the MANDATORY check a no-op

context