Files
spring-boot-demo/resilience/docs/05-retry-and-transactions.md
T
asmhatreandClaude Opus 5 7e1676c763 Add resilience: Spring Framework 7 @Retryable and @ConcurrencyLimit
Companion code for "Spring Framework 7's Built-in Resilience: @Retryable,
@ConcurrencyLimit, and What's Left for Resilience4j". Every retry counted
by recording real invocations: defaults, backoff and jitter, timeout,
reactive and CompletableFuture returns, the concurrency limit's BLOCK and
REJECT policies, retries around transactions, composition with
Resilience4j 2.4.0, and the annotation API across 7.0.0-7.0.9.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C3TETMrqVUWeFkNtz3Jbo3
2026-09-11 17:12:25 +00:00

2.5 KiB

5. Retry and transactions

← 4. @ConcurrencyLimit · Index · Next: 6. What is left for Resilience4j →

StockWriter.record is @Retryable @Transactional, inserts a row, and fails twice.

Which interceptor is outside

RetryAnnotationBeanPostProcessor's constructor calls setBeforeExistingAdvisors(true) (javap -c on spring-context 7.0.9). The transaction advisor is applied first by the auto-proxy creator; the retry advisor is then inserted at index 0 (proxy-stock-writer.txt). The order attribute of @EnableResilientMethods is the post-processor's order, not the advice's position, and does not change this.

Alone: each attempt is its own transaction

tx-standalone.txt - three connection holders, one row:

    "attempts": [
        "attempt 1: transaction active=true, connection holder @6aa73704",
        "attempt 2: transaction active=true, connection holder @1978b580",
        "attempt 3: transaction active=true, connection holder @7d5534c2"
    ],
    "rows in stock_movement": 1

This is what you want: the failed attempts rolled back their inserts.

Called from inside a transaction: the retry cannot help

OrderFacade.placeOrder is @Transactional and calls the same method (tx-joined.txt):

    "caller got": "org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only",
    "attempts": [
        "attempt 1: transaction active=true, connection holder @6135f416",
        "attempt 2: transaction active=true, connection holder @6135f416",
        "attempt 3: transaction active=true, connection holder @6135f416"
    ],
    "rows in stock_movement": 0

Every attempt joins the outer transaction (REQUIRED). The first failure passes through the inner TransactionInterceptor, which marks the shared transaction rollback-only. The third attempt succeeds, the retry returns normally, and the outer commit throws. Three attempts, zero rows, an exception the caller did not expect. The retry belongs at the outermost transactional boundary - or the inner method needs REQUIRES_NEW. The @Transactional article covers the rollback-only mechanics.