Spring 7's Native Retry & Concurrency Limit: When They're Enough (and When They're Not)
Spring Framework 7 introduces @Retryable and @ConcurrencyLimit annotations for method-level retry and concurrency control, enabling developers to handle transient failures and limit parallel executions without third-party libraries, though they operate per-JVM and lack distributed coordination.
Spring Framework 7 adds two built-in fault-tolerance capabilities: method-level retry via @Retryable and concurrency limiting via @ConcurrencyLimit. For projects that only need these two features, a third-party resilience library may no longer be required. However, this does not make libraries like Resilience4j (strong in circuit breaking, rate limiting, and timeouts) or Sentinel (focused on traffic governance) obsolete; the choice should be driven by specific requirements.
How to Use
A typical microservice calling a database can suffer when a downstream query slows: request threads pile up, the connection pool is exhausted, and memory and latency degrade, dragging down otherwise healthy endpoints. Previously, teams used Resilience4j's Bulkhead and RateLimiter to control pressure. Spring 7.0 now provides @ConcurrencyLimit and @Retryable for common scenarios.
Before using these annotations, you must explicitly enable method-level resilience processing:
import org.springframework.context.annotation.Configuration;
import org.springframework.resilience.annotation.EnableResilientMethods;
@Configuration
@EnableResilientMethods
public class ResilienceConfiguration {
}1. Use @ConcurrencyLimit to Limit Concurrent Calls
The idea is straightforward: only a fixed number of calls may enter the target method at the same time. For example, a month-end batch that generates merchant settlement statements by joining orders, refunds, and fees is expensive and should not run hundreds of times concurrently.
import java.time.YearMonth;
import org.springframework.resilience.annotation.ConcurrencyLimit;
import org.springframework.resilience.annotation.ConcurrencyLimit.ThrottlePolicy;
import org.springframework.stereotype.Service;
@Service
public class SettlementService {
private final SettlementRepository repository;
public SettlementService(SettlementRepository repository) {
this.repository = repository;
}
@ConcurrencyLimit(limit = 10, policy = ThrottlePolicy.REJECT)
public MonthlyStatement generateMonthlyStatement(Long merchantId, YearMonth month) {
return repository.buildMonthlyStatement(merchantId, month);
}
}A common pitfall: @ConcurrencyLimit defaults to BLOCK policy, meaning excess calls wait instead of being rejected. You must explicitly set policy = ThrottlePolicy.REJECT to have the framework throw InvocationRejectedException. Callers can catch this exception and execute fallback logic (e.g., return cached data); the annotation itself has no fallbackMethod parameter.
The concurrency limit can be guided by the HikariCP pool size, but they are not directly equivalent — a single business call may use zero, one, or multiple connections. A safer approach is to set the limit based on load-test results while leaving headroom for other endpoints.
2. Use Native @Retryable for Transient Failures
While concurrency limiting blocks excess traffic, retry handles a different class of problems. For instance, an order service may call a logistics platform to calculate shipping costs; occasional connection failures or timeouts are usually short-lived, and a retry often succeeds.
Spring Framework 7's @Retryable lives in org.springframework.resilience.annotation. It draws inspiration from Spring Retry but uses a redesigned core API:
import java.net.ConnectException;
import java.net.http.HttpTimeoutException;
import org.springframework.resilience.annotation.ConcurrencyLimit;
import org.springframework.resilience.annotation.ConcurrencyLimit.ThrottlePolicy;
import org.springframework.resilience.annotation.Retryable;
import org.springframework.stereotype.Service;
@Service
public class ShippingQuoteService {
private final LogisticsClient logisticsClient;
public ShippingQuoteService(LogisticsClient logisticsClient) {
this.logisticsClient = logisticsClient;
}
@Retryable(
includes = {
ConnectException.class,
HttpTimeoutException.class
},
maxRetries = 2,
delay = 200,
multiplier = 2.0,
maxDelay = 2000
)
@ConcurrencyLimit(limit = 50, policy = ThrottlePolicy.REJECT)
public ShippingQuote queryShippingQuote(ShippingQuoteRequest request)
throws ConnectException, HttpTimeoutException {
return logisticsClient.queryQuote(request);
}
} maxRetries = 2means up to two retries after the initial failure (three total attempts). delay = 200 sets the first wait to 200 ms, with subsequent waits multiplied by 2.0, capped at 2 seconds.
The example only includes ConnectException and HttpTimeoutException in includes. The InvocationRejectedException thrown when the concurrency limit is reached is deliberately excluded, so retries do not amplify pressure.
The choice of a shipping-quote query is intentional: it is a read-only operation, so duplicate calls do not create extra business data. If the operation were non-idempotent — creating a shipment, deducting inventory, or initiating a payment — you must first guard against duplicate submissions with a business ID or idempotency key before considering automatic retry; otherwise, a technically successful retry could produce duplicate business records.
Do not assume the two annotations execute in a fixed nesting order based on their source-code order. The Spring documentation provides no such guarantee. If the execution order affects capacity or correctness, you must define boundaries through exception filtering, explicit proxy configuration, and concurrency testing.
Summary
A critical boundary: both annotations rely on Spring proxies within the current application. Retry state and concurrency counts exist only inside the current JVM; instances do not share state. If a service runs 10 replicas, each configured with @ConcurrencyLimit(10), the cluster can theoretically handle 100 concurrent calls, not a global limit of 10.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Architecture Diary
Committed to sharing original, high‑quality technical articles; no fluff or promotional content.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
