package com.example.rates.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.web.client.RestTemplate;
@Configuration
@EnableRetry
public class RetryConfig {
@Bean
public RestTemplate rateRestTemplate() {
return new RestTemplate();
}
}
package com.example.rates.client;
import com.example.rates.error.TransientUpstreamException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
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.RestTemplate;
import java.math.BigDecimal;
@Component
public class ExchangeRateClient {
private static final Logger log = LoggerFactory.getLogger(ExchangeRateClient.class);
private final RestTemplate restTemplate;
public ExchangeRateClient(RestTemplate rateRestTemplate) {
this.restTemplate = rateRestTemplate;
}
@Retryable(
retryFor = { ResourceAccessException.class, TransientUpstreamException.class },
maxAttempts = 4,
backoff = @Backoff(delay = 200, multiplier = 2.0, random = true)
)
public BigDecimal fetchRate(String base, String quote) {
String url = "https://api.fx-provider.example/v1/rate?base={base}"e={quote}";
log.info("Fetching rate {} -> {}", base, quote);
ResponseEntity<RateResponse> response =
restTemplate.getForEntity(url, RateResponse.class, base, quote);
if (response.getStatusCode().is5xxServerError()) {
throw new TransientUpstreamException(
"Upstream returned " + response.getStatusCode());
}
return response.getBody().getRate();
}
@Recover
public BigDecimal recoverRate(Throwable cause, String base, String quote) {
log.warn("Retries exhausted for {} -> {}, using last-known rate: {}",
base, quote, cause.getMessage());
return LastKnownRates.lookup(base, quote);
}
}
package com.example.rates.error;
public class TransientUpstreamException extends RuntimeException {
public TransientUpstreamException(String message) {
super(message);
}
public TransientUpstreamException(String message, Throwable cause) {
super(message, cause);
}
}
package com.example.rates.web;
import com.example.rates.client.ExchangeRateClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
import java.util.Map;
@RestController
public class CurrencyController {
private final ExchangeRateClient rateClient;
public CurrencyController(ExchangeRateClient rateClient) {
this.rateClient = rateClient;
}
@GetMapping("/convert")
public Map<String, Object> convert(
@RequestParam String base,
@RequestParam String quote,
@RequestParam BigDecimal amount) {
BigDecimal rate = rateClient.fetchRate(base, quote);
BigDecimal converted = amount.multiply(rate);
return Map.of(
"base", base,
"quote", quote,
"rate", rate,
"amount", amount,
"converted", converted);
}
}
This snippet shows how Spring Retry's declarative annotations wrap a flaky outbound HTTP call so transient failures are retried automatically, with a guaranteed fallback path when the retries are exhausted. The pattern targets the common reality that networks, upstream services, and load balancers occasionally return 5xx, time out, or drop connections — failures that usually succeed on a second attempt but should not crash the calling request.
In RetryConfig, @EnableRetry turns on the aspect that intercepts @Retryable methods. Nothing else is required to activate the feature, but this class exists to make the capability explicit and to allow a shared RetryTemplate bean elsewhere if programmatic retry is ever needed.
ExchangeRateClient is where the policy lives. The @Retryable annotation names the exact exceptions worth retrying — ResourceAccessException for timeouts and connection resets, and a custom TransientUpstreamException — while deliberately excluding others so that programming errors or 4xx client mistakes fail fast instead of being retried pointlessly. maxAttempts = 4 counts the first call plus three retries, and @Backoff adds exponential delay with multiplier = 2.0 and a small random jitter to avoid thundering-herd retries where many clients wake up in lockstep. Inside fetchRate, a 5xx response is translated into TransientUpstreamException so the retry aspect can see a retryable signal rather than a generic status.
The @Recover method recoverRate is the fallback. Spring Retry calls it only after the final attempt fails, and it matches by having a compatible throwable as its first parameter followed by the same argument list as the retried method. Here it returns a cached or last-known rate rather than propagating the failure, keeping the caller resilient. A subtle pitfall the code respects: the @Recover signature must line up with @Retryable or Spring silently rethrows the original exception, so recoverRate mirrors fetchRate exactly.
CurrencyController shows the payoff — it simply calls fetchRate and stays oblivious to the retry machinery. The trade-off is that retries add latency and can amplify load on an already struggling upstream, which is why bounded attempts, backoff, jitter, and a fast fallback all matter together.
Related snips
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
createdAt: String!
GraphQL API with Spring Boot
package com.example.starter.config;
import com.example.starter.properties.CustomProperties;
import com.example.starter.service.CustomService;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
Custom Spring Boot starters
import React from "react";
type FallbackProps = {
error: Error;
reset: () => void;
};
React Error Boundary + error reporting hook
package com.example.demo.config;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.kafka.common.serialization.StringSerializer;
Messaging with Apache Kafka
class FeatureFlag < ApplicationRecord
validates :key, presence: true, uniqueness: true
validates :percentage, inclusion: { in: 0..100 }
after_commit :expire_cache
Safer Feature Flagging: Cache + DB Fallback
Share this code
Here's the card — post it anywhere.