Retry Pattern in Microservices
Building resilient microservices that recover from temporary failures. In a microservices architecture, the simple act of one service calling another can fail for many reasons. The Retry pattern lets a service automatically try again when a failure is likely to be temporary, so users rarely notice the hiccup.
This guide explains when retries help and when they hurt, the building blocks of a good retry strategy, and working Java examples: plain Java with exponential backoff and jitter, Resilience4j, Spring’s @Retryable, idempotency keys, and retries in Kafka consumers.
1. Why Remote Calls Fail
In a monolith, most calls are method calls inside one process, and they almost never fail. In a microservices system, the same calls travel over the network, which is far less reliable:
- A network glitch drops or delays a packet.
- A service is briefly overloaded and responds slowly.
- An instance is restarting during a deployment.
- A database is failing over to a replica.
- A rate limiter temporarily rejects a burst of requests.
Many of these problems last only milliseconds or seconds. Returning an error to the user for such a short hiccup creates a poor experience, and that is exactly what the Retry pattern prevents.
2. What Is the Retry Pattern?
The Retry pattern is a resilience strategy in which an application automatically repeats a failed operation a limited number of times, waiting a little between attempts.
The core idea: many failures are temporary (transient). Instead of immediately returning an error, the calling service waits briefly and tries again. Often the next attempt succeeds, and the operation completes as if nothing had gone wrong.
However, not all failures are temporary. A bug in the code or an invalid request will fail every time, no matter how often you retry. The Retry pattern must therefore be applied intelligently, or it will make problems worse.
3. Transient vs Permanent Failures
| Transient (retry) | Permanent (do not retry) | |
|---|---|---|
| Meaning | Likely to succeed if tried again soon | Will fail the same way every time |
| Network | Connection reset, connection timeout, read timeout | Unknown host because of a wrong configuration |
| HTTP status codes | 502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout, 429 Too Many Requests (after waiting) | 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 422 Unprocessable Content |
| Database | Deadlock, lock timeout, failover in progress | Constraint violation, syntax error |
| Typical Java exceptions | ConnectException, SocketTimeoutException, ResourceAccessException | IllegalArgumentException, validation errors, HttpClientErrorException (4xx) |
500 Internal Server Error is a grey area: it may be a temporary problem or a bug. Retry it at most once or twice, and investigate if it keeps happening.
4. Key Components of a Retry Strategy
A naive retry, trying again immediately and forever, can do more harm than good. A well-designed retry strategy has four parts:
| Component | What it does | Example |
|---|---|---|
| Retry limit (max attempts) | Caps how many times an operation is tried, so a request that will never succeed does not consume resources forever | 3 attempts in total |
| Backoff strategy | Decides how long to wait between attempts, giving the struggling service time to recover | Fixed: 1 s, 1 s, 1 s · Exponential: 1 s, 2 s, 4 s |
| Jitter (randomness) | Adds a random amount to each wait, so many clients do not retry at exactly the same moment and cause a “retry storm” | Exponential with ±50% randomness: 0.7 s, 2.4 s, 3.1 s |
| Failure conditions | Defines which errors are worth retrying | Retry timeouts and 503; never retry 400 or 404 |
Why exponential backoff? Each retry gives the failing service more and more time to recover, while the total number of attempts stays small:
| Attempt | Fixed backoff (1 s) | Exponential backoff (1 s × 2) | Exponential + jitter |
|---|---|---|---|
| 1st call | — | — | — |
| Wait before 2nd | 1 s | 1 s | somewhere between 0.5 s and 1.5 s |
| Wait before 3rd | 1 s | 2 s | somewhere between 1 s and 3 s |
| Wait before 4th | 1 s | 4 s | somewhere between 2 s and 6 s |
Always cap the maximum wait, for example at 10 seconds, so the exponential growth does not keep users waiting for minutes.
5. A Practical Example: Order Service and Payment Service
5.1 Scenario without the Retry pattern
- A user places an order, and the order service receives the request.
- The order service calls the payment service to take the payment.
- At that exact moment, the payment service is under heavy load, or a brief network problem causes the request to time out.
- The order service immediately returns “Payment failed” to the user.
- The user is frustrated, even though their card is valid, and may try again, creating duplicate order attempts.
5.2 Scenario with the Retry pattern
- The order service calls the payment service, and the request times out.
- Instead of failing, it enters its retry logic and waits about 1 second (the first step of an exponential backoff).
- It calls the payment service again, sending the same idempotency key, so the customer can never be charged twice (section 9).
- This time the network problem is gone, and the payment succeeds.
- The order is created, and the user sees a success message without ever knowing about the hiccup.
Order Service Payment Service
|---- attempt 1: charge(order-1001) --->| (timeout)
| x
| wait ~1 s
|---- attempt 2: charge(order-1001) --->|
|<--------------- 200 OK ---------------| (charged once)
|
create order, reply "Order placed"
6. Retry with Backoff and Jitter in Plain Java
To understand the mechanics, here is a small, reusable retry helper in plain Java, with a retry limit, exponential backoff, jitter, a maximum wait and a check for which errors to retry:
package com.example.resilience;
import java.time.Duration;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.Predicate;
import java.util.function.Supplier;
public class RetryExecutor {
private final int maxAttempts;
private final Duration initialDelay;
private final double multiplier;
private final Duration maxDelay;
private final Predicate<Exception> isRetryable;
public RetryExecutor(int maxAttempts, Duration initialDelay, double multiplier,
Duration maxDelay, Predicate<Exception> isRetryable) {
this.maxAttempts = maxAttempts;
this.initialDelay = initialDelay;
this.multiplier = multiplier;
this.maxDelay = maxDelay;
this.isRetryable = isRetryable;
}
public <T> T execute(Supplier<T> operation) {
for (int attempt = 1; ; attempt++) {
try {
return operation.get();
} catch (RuntimeException ex) {
// Give up on permanent errors or after the last attempt
if (!isRetryable.test(ex) || attempt >= maxAttempts) {
throw ex;
}
Duration wait = delayBefore(attempt + 1);
System.out.printf("Attempt %d failed (%s). Retrying in %d ms%n",
attempt, ex.getClass().getSimpleName(), wait.toMillis());
sleep(wait);
}
}
}
// Exponential backoff with jitter: a random wait between half and the full backoff value
private Duration delayBefore(int nextAttempt) {
double backoff = initialDelay.toMillis() * Math.pow(multiplier, nextAttempt - 2);
long capped = (long) Math.min(backoff, maxDelay.toMillis());
long withJitter = ThreadLocalRandom.current().nextLong(capped / 2, capped + 1);
return Duration.ofMillis(withJitter);
}
private static void sleep(Duration duration) {
try {
Thread.sleep(duration.toMillis());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Retry interrupted", e);
}
}
}
Using it for the payment call from the example:
RetryExecutor retry = new RetryExecutor(
3, // at most 3 attempts in total
Duration.ofSeconds(1), // first wait about 1 second
2.0, // then about 2 seconds
Duration.ofSeconds(10), // never wait longer than 10 seconds
ex -> ex instanceof ResourceAccessException // timeouts, connection errors
|| ex instanceof HttpServerErrorException.ServiceUnavailable); // 503
PaymentResult result = retry.execute(
() -> paymentClient.charge(order.id(), order.total()));
This version uses a slightly gentler jitter: each wait is a random value between half and the full backoff. In real projects, prefer a proven library such as Resilience4j, which adds configuration, metrics and integration with circuit breakers.
7. Retry with Resilience4j in Spring Boot
Resilience4j is the standard resilience library for Spring Boot. Add io.github.resilience4j:resilience4j-spring-boot3 and Spring AOP to your project, then configure a retry instance:
resilience4j:
retry:
instances:
payment:
max-attempts: 3 # includes the first call
wait-duration: 1s # first wait
enable-exponential-backoff: true
exponential-backoff-multiplier: 2 # 1s, then 2s
retry-exceptions:
- org.springframework.web.client.ResourceAccessException
- org.springframework.web.client.HttpServerErrorException
ignore-exceptions:
- org.springframework.web.client.HttpClientErrorException
package com.example.order.client;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
import io.github.resilience4j.retry.annotation.Retry;
@Component
public class PaymentClient {
private final RestClient restClient;
public PaymentClient(RestClient.Builder builder) {
this.restClient = builder.baseUrl("http://payment-service").build();
}
@Retry(name = "payment", fallbackMethod = "paymentPending")
public PaymentResult charge(ChargeRequest request) {
return restClient.post()
.uri("/payments")
// The same key on every attempt: the payment service charges only once
.header("Idempotency-Key", request.orderId())
.body(request)
.retrieve()
.body(PaymentResult.class);
}
// Called after all attempts have failed
private PaymentResult paymentPending(ChargeRequest request, Exception ex) {
return PaymentResult.pending(request.orderId(),
"Payment is being confirmed. We will email you shortly.");
}
}
Exceptions listed under retry-exceptions are retried; those under ignore-exceptions, such as 4xx client errors, fail immediately. The fallback runs only after the last attempt fails, and here it returns a friendly “pending” result instead of an error.
7.1 Adding jitter
To combine exponential backoff with randomness, configure the interval in Java:
package com.example.order.config;
import java.time.Duration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.github.resilience4j.common.retry.configuration.RetryConfigCustomizer;
import io.github.resilience4j.core.IntervalFunction;
@Configuration
public class RetryConfiguration {
@Bean
public RetryConfigCustomizer paymentRetryJitter() {
return RetryConfigCustomizer.of("payment", builder -> builder
.intervalFunction(IntervalFunction.ofExponentialRandomBackoff(
Duration.ofSeconds(1), // initial wait
2.0, // multiplier
0.5))); // +/- 50% randomness
}
}
8. Declarative Retries with @Retryable
Spring offers its own declarative retries. With the Spring Retry library (org.springframework.retry:spring-retry plus Spring AOP), add @EnableRetry to a configuration class and annotate methods:
package com.example.order.client;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Component;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestClient;
@Component
public class InventoryClient {
private final RestClient restClient;
public InventoryClient(RestClient.Builder builder) {
this.restClient = builder.baseUrl("http://inventory-service").build();
}
@Retryable(retryFor = ResourceAccessException.class,
maxAttempts = 3,
backoff = @Backoff(delay = 500, multiplier = 2, random = true))
public StockStatus checkStock(String productId) {
return restClient.get()
.uri("/stock/{id}", productId)
.retrieve()
.body(StockStatus.class);
}
// Runs when all attempts have failed
@Recover
public StockStatus stockUnknown(ResourceAccessException ex, String productId) {
return StockStatus.unknown(productId);
}
}
@Retryable annotation in the org.springframework.resilience.annotation package, enabled with @EnableResilientMethods, so a separate library is no longer needed for simple cases. The ideas, limits, backoff and which exceptions to retry, are the same.9. Idempotency: Making Retries Safe
A retry sends the same request again. If the first request actually succeeded but the response was lost on the way back, a retry could repeat the action: charge a card twice, create two orders, or send two emails.
The solution is idempotency: performing an operation several times must have the same effect as performing it once.
| Operation | Naturally idempotent? | Safe to retry? |
|---|---|---|
GET /orders/42 | Yes, it only reads | Yes |
PUT /orders/42/status with SHIPPED | Yes, it sets a value | Yes |
DELETE /cart/items/7 | Yes, deleting twice leaves the same result | Yes |
POST /payments | No, each call could create a new charge | Only with an idempotency key |
For non-idempotent operations, the client sends a unique idempotency key, such as the order ID, with every attempt. The server remembers which keys it has already processed and returns the original result instead of repeating the action:
@Service
public class PaymentService {
private final PaymentRepository payments;
private final CardProcessor cardProcessor;
public PaymentService(PaymentRepository payments, CardProcessor cardProcessor) {
this.payments = payments;
this.cardProcessor = cardProcessor;
}
@Transactional
public PaymentResult charge(String idempotencyKey, ChargeRequest request) {
// Already processed? Return the stored result instead of charging again
Optional<Payment> existing = payments.findByIdempotencyKey(idempotencyKey);
if (existing.isPresent()) {
return PaymentResult.from(existing.get());
}
String transactionId = cardProcessor.charge(request.cardToken(), request.amount());
Payment payment = payments.save(new Payment(idempotencyKey, transactionId, request.amount()));
return PaymentResult.from(payment);
}
}
Put a unique constraint on the idempotency key column, so even two retries arriving at the same moment cannot both create a payment.
10. Combining Retry with Timeout and Circuit Breaker
Retry is most effective as part of a team of resilience patterns:
| Pattern | Role with retries |
|---|---|
| Timeout | Makes each attempt fail fast instead of hanging, so retries can happen in reasonable time |
| Circuit breaker | When retries keep failing, the circuit opens and stops all calls for a while, giving the service time to recover |
| Bulkhead | Limits how many calls, including retries, can run at once |
| Fallback | Returns a useful answer after the last attempt fails |
@Retry(name = "payment")
@CircuitBreaker(name = "payment", fallbackMethod = "paymentPending")
@TimeLimiter(name = "payment")
public CompletableFuture<PaymentResult> charge(ChargeRequest request) {
return CompletableFuture.supplyAsync(() -> paymentClient.charge(request));
}
Resilience4j applies these from the outside in: Retry → CircuitBreaker → RateLimiter → TimeLimiter → Bulkhead. Each retry attempt therefore goes through the circuit breaker; once the circuit is open, attempts fail instantly with CallNotPermittedException, which you should usually not retry. Add it to ignore-exceptions so the retry stops at once.
11. Avoiding Retry Storms
Retries can multiply load exactly when a system is weakest. If every layer retries, the effect grows quickly:
Client -> retries 3 times
API Gateway -> retries 3 times each
Order Service -> retries 3 times each
Payment Service receives up to 3 x 3 x 3 = 27 requests
for ONE user click - while it is already struggling.
How to prevent retry storms:
- Retry in one layer only, usually the one closest to the failing call. Other layers should fail fast.
- Keep attempts low: 2–3 in total is usually enough.
- Always use exponential backoff with jitter.
- Use a circuit breaker so retries stop when a service is clearly down.
- Use a retry budget: allow retries only while they make up a small share, for example 10%, of total traffic.
- Respect
Retry-Afterheaders on429and503responses.
12. Retries in Messaging with Kafka
Retries are just as important when consuming messages. If a Kafka listener throws an exception, Spring for Apache Kafka can retry the message with backoff and then move it to a dead-letter topic, so one bad message does not block the others:
package com.example.shipping.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.listener.DeadLetterPublishingRecoverer;
import org.springframework.kafka.listener.DefaultErrorHandler;
import org.springframework.kafka.support.ExponentialBackOffWithMaxRetries;
@Configuration
public class KafkaErrorHandlingConfig {
@Bean
public DefaultErrorHandler errorHandler(KafkaTemplate<Object, Object> template) {
// After the retries fail, publish the message to "<topic>-dlt"
DeadLetterPublishingRecoverer recoverer = new DeadLetterPublishingRecoverer(template);
ExponentialBackOffWithMaxRetries backOff = new ExponentialBackOffWithMaxRetries(3);
backOff.setInitialInterval(1_000); // 1 s
backOff.setMultiplier(2.0); // 2 s, 4 s
backOff.setMaxInterval(10_000);
DefaultErrorHandler handler = new DefaultErrorHandler(recoverer, backOff);
// Bad data will never succeed: send it to the dead-letter topic immediately
handler.addNotRetryableExceptions(IllegalArgumentException.class);
return handler;
}
}
Spring Boot picks up this error handler for the default listener container. As with HTTP calls, the listener should be idempotent, because a message may be delivered more than once. See Asynchronous Communication in Microservices.
13. Logging and Monitoring Retries
Retries hide problems from users, but they should never hide them from you. Log every retry and track metrics:
@Configuration
public class RetryLoggingConfig {
private static final Logger log = LoggerFactory.getLogger(RetryLoggingConfig.class);
public RetryLoggingConfig(RetryRegistry retryRegistry) {
retryRegistry.retry("payment").getEventPublisher()
.onRetry(event -> log.warn("Retry {} for '{}' after {}",
event.getNumberOfRetryAttempts(),
event.getName(),
event.getLastThrowable().toString()))
.onError(event -> log.error("All retries failed for '{}'", event.getName()));
}
}
| Metric (Resilience4j via Actuator) | What it tells you |
|---|---|
resilience4j.retry.calls with kind=successful_without_retry | Healthy calls |
resilience4j.retry.calls with kind=successful_with_retry | Calls saved by retrying; a rising number signals an unstable dependency |
resilience4j.retry.calls with kind=failed_with_retry | Calls that failed even after all retries |
resilience4j.retry.calls with kind=failed_without_retry | Calls that failed with a non-retryable error |
Use these numbers to spot unstable services and to tune your policies, for example by changing the number of attempts or the backoff.
14. Best Practices
- Retry only transient failures: timeouts, connection errors,
502,503,504. Never retry client errors such as400,401,403or404. - Make operations idempotent, or use idempotency keys for anything that changes data.
- Limit attempts: usually 2–3 in total.
- Use exponential backoff with jitter, and cap the maximum wait.
- Set a timeout on every attempt and an overall time budget for the whole operation.
- Retry in one layer only to avoid multiplying load.
- Combine with a circuit breaker, so retries stop when a service is clearly down.
- Provide a fallback for when all attempts fail.
- Respect
Retry-Afterheaders from the server. - Log and monitor retries, and alert when they increase.
15. Common Mistakes to Avoid
| Mistake | What goes wrong | Better approach |
|---|---|---|
| Retrying immediately with no wait | Hammers a struggling service and makes it worse | Use exponential backoff with jitter |
| Retrying forever | Threads and requests pile up; the user waits endlessly | Limit attempts and set an overall deadline |
| Retrying 4xx errors | Wasted calls that can never succeed | Retry only transient failures |
| Retrying non-idempotent operations | Double charges, duplicate orders or emails | Use idempotency keys |
| Retrying at every layer | One click becomes dozens of requests | Retry in one place; fail fast elsewhere |
| No timeout per attempt | Each attempt can hang for a long time | Set connect and read timeouts |
| Silent retries | Unstable services go unnoticed until they fail completely | Log retries and monitor metrics |
| Retrying when the circuit is open | Pointless attempts that fail instantly | Do not retry CallNotPermittedException |
16. Summary
The Retry pattern is a simple yet powerful tool for building resilient microservices. By automatically repeating operations that fail for temporary reasons, it turns network glitches, short overloads and restarts into invisible hiccups instead of user-facing errors. Implement it carefully: retry only transient failures, limit attempts, use exponential backoff with jitter, and make operations idempotent with idempotency keys. Combine retries with timeouts, circuit breakers and fallbacks, retry in one layer only, and monitor every retry. Done well, retries transform a fragile distributed system into one that heals itself from the everyday storms of distributed computing. Continue with the Circuit Breaker pattern to learn when to stop retrying.
17. Frequently Asked Questions (FAQ)
Q1. What is the Retry pattern in simple words?
The Retry pattern means automatically trying a failed operation again, a limited number of times, after a short wait. It is used for failures that are likely to be temporary, such as a network glitch or a service that is briefly busy.
Often the second or third attempt succeeds, and the user never notices that anything went wrong.
Q2. Which errors should be retried?
Retry transient errors that may succeed on a later attempt:
- Connection failures and timeouts.
502 Bad Gateway,503 Service Unavailableand504 Gateway Timeout.429 Too Many Requests, after waiting as long asRetry-Aftersays.- Database deadlocks and lock timeouts.
Do not retry client errors such as 400, 401, 403, 404 or validation failures; they will fail the same way every time.
Q3. How many times should I retry?
Usually 2 or 3 attempts in total, including the first call. Most transient failures clear up within that time.
More attempts rarely help: if a service is still failing after a few seconds, it is probably down, and further retries only add load and keep the user waiting. Let a circuit breaker and a fallback take over instead.
Q4. What is exponential backoff?
Exponential backoff means the wait between attempts doubles (or grows by another factor) each time: for example 1 second, then 2, then 4.
Early retries happen quickly in case the problem was tiny, while later retries give the failing service more and more time to recover. Always set a maximum wait so the delays do not grow too large.
Q5. What is jitter, and why is it important?
Jitter adds a random amount of time to each wait. Without it, many clients that failed at the same moment would all retry at the same moment too, creating waves of traffic that knock the service over again.
With jitter, retries are spread out over time, so the recovering service receives a smooth trickle of requests instead of sudden spikes.
Q6. What does idempotent mean, and why does it matter for retries?
An operation is idempotent if doing it several times has the same effect as doing it once. Reading data and setting a value are idempotent; creating a payment usually is not.
It matters because a request may have succeeded even though the response was lost. Retrying a non-idempotent operation could then charge a customer twice. Make operations idempotent, for example with idempotency keys, before retrying them.
Q7. What is an idempotency key?
An idempotency key is a unique value the client sends with a request that changes data, often in an Idempotency-Key header. Every retry of the same request uses the same key.
The server stores the keys it has processed. If a request arrives with a key it has already seen, it returns the stored result instead of doing the work again. The order ID is a natural key for paying for an order.
Q8. What is a retry storm?
A retry storm happens when many clients, or many layers of the same system, retry at once and multiply the load on a service that is already struggling.
For example, if the client, the gateway and a service each retry 3 times, one user action can turn into 27 requests. Prevent it by retrying in one layer only, using backoff with jitter, limiting attempts and adding a circuit breaker.
Q9. What is the difference between retry and circuit breaker?
- Retry assumes the failure is temporary and tries again.
- Circuit breaker detects that failures are persistent and stops calling the service for a while.
They work best together: retries handle brief glitches, and when retries keep failing, the circuit breaker opens so the system fails fast and gives the service time to recover.
Q10. Should I retry on a timeout?
Usually yes, because timeouts are often caused by short slowdowns. But be careful:
- A timeout does not tell you whether the server finished the work, so the operation must be idempotent.
- If a service is consistently slow, retrying adds more load; a circuit breaker should stop the calls.
- Keep an overall time budget, so several timeouts in a row do not keep the user waiting too long.
Q11. Where should retries happen: client, gateway or service?
Retry in one place, usually as close as possible to the call that fails, for example in the service that calls the payment provider.
Other layers should fail fast. If the gateway, the service and the client all retry, the number of requests multiplies and can turn a small problem into an outage.
Q12. Should I retry 429 Too Many Requests responses?
Yes, but only after waiting. A 429 means you are sending too much. Read the Retry-After header and wait at least that long before trying again.
Retrying immediately makes the situation worse and may get your client blocked. If no header is present, use exponential backoff with jitter.
Q13. Does Resilience4j max-attempts include the first call?
Yes. In Resilience4j, max-attempts: 3 means one initial call plus up to two retries. The default is 3.
Other libraries count differently: some configure the number of retries rather than total attempts. Always check the documentation, so you do not accidentally double the load.
Q14. How do retries affect response time for users?
Each retry adds the wait time plus the time of another attempt. With a 2-second timeout and waits of 1 and 2 seconds, three failed attempts can take about 9 seconds.
Set an overall deadline for user-facing requests, keep attempts low, and prefer a quick fallback, such as “your payment is being confirmed”, over a long wait.
Q15. How should retries work for Kafka or RabbitMQ consumers?
Retry a failed message a few times with backoff, then move it to a dead-letter topic or queue so it does not block the messages behind it.
- In Spring for Apache Kafka, use
DefaultErrorHandlerwith a backoff and aDeadLetterPublishingRecoverer. - In RabbitMQ, use retry settings plus a dead-letter exchange.
- Mark bad-data exceptions as not retryable, and make consumers idempotent.
Q16. Is it safe to retry database transactions?
Retrying the whole transaction is safe and common for deadlocks, serialization failures and optimistic locking conflicts, because the failed transaction was rolled back completely.
Do not retry individual statements inside a failed transaction, and make sure no external side effects, such as sending emails, happen before the transaction commits.
Q17. What should happen after the last retry fails?
Decide this up front. Common options are:
- Return a fallback, such as cached data or a default value.
- Accept the request and finish it later, for example “payment pending”, with a background job or message.
- Return a clear error, such as
503, with a helpful message.
Always log the final failure, so the team can investigate.
Q18. Can I retry inside a database transaction?
Avoid retrying remote calls inside a long-running database transaction. Every wait keeps the transaction open, holding locks and connections, which can slow down the whole database.
Instead, make the remote call before or after the transaction, or use events and the outbox pattern, so the transaction stays short.
Q19. How do I test retry logic?
- Unit test the policy: mock a client that fails twice and then succeeds, and check it was called three times.
- Check that non-retryable errors, such as
400, are not retried. - Use WireMock scenarios to return
503and then200in integration tests. - Use short wait times in tests, so they stay fast.
Q20. Which Java libraries support retries?
| Library | Highlights |
|---|---|
| Resilience4j | Retry with backoff and jitter, plus circuit breaker, bulkhead, rate limiter and metrics |
| Spring Retry | @Retryable and @Recover annotations and a RetryTemplate |
| Spring Framework 7 | Built-in @Retryable for simple method retries |
| Spring for Apache Kafka | Message retries with backoff and dead-letter topics |
| Failsafe | A lightweight, framework-independent resilience library |