@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "downstreamExecutor")
public ThreadPoolTaskExecutor downstreamExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(8);
executor.setMaxPoolSize(16);
executor.setQueueCapacity(50);
executor.setThreadNamePrefix("downstream-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
@Component
public class ProfileClient {
private static final Logger log = LoggerFactory.getLogger(ProfileClient.class);
private final RestTemplate restTemplate;
public ProfileClient(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@Async("downstreamExecutor")
public CompletableFuture<UserDto> fetchUser(long userId) {
UserDto user = restTemplate.getForObject("http://users/api/{id}", UserDto.class, userId);
return CompletableFuture.completedFuture(user);
}
@Async("downstreamExecutor")
public CompletableFuture<List<OrderDto>> fetchOrders(long userId) {
OrderDto[] orders = restTemplate.getForObject("http://orders/api?user={id}", OrderDto[].class, userId);
return CompletableFuture.completedFuture(Arrays.asList(orders));
}
@Async("downstreamExecutor")
public CompletableFuture<PreferencesDto> fetchPreferences(long userId) {
return CompletableFuture
.completedFuture(restTemplate.getForObject("http://prefs/api/{id}", PreferencesDto.class, userId))
.exceptionally(ex -> {
log.warn("preferences lookup failed for {}, using defaults", userId, ex);
return PreferencesDto.defaults();
});
}
}
@Service
public class ProfileAggregationService {
private final ProfileClient client;
public ProfileAggregationService(ProfileClient client) {
this.client = client;
}
public AggregatedProfile buildProfile(long userId) {
CompletableFuture<UserDto> userFuture = client.fetchUser(userId);
CompletableFuture<List<OrderDto>> ordersFuture = client.fetchOrders(userId);
CompletableFuture<PreferencesDto> prefsFuture = client.fetchPreferences(userId);
CompletableFuture<Void> all = CompletableFuture.allOf(userFuture, ordersFuture, prefsFuture);
try {
all.get(2, TimeUnit.SECONDS);
} catch (TimeoutException e) {
userFuture.cancel(true);
ordersFuture.cancel(true);
prefsFuture.cancel(true);
throw new ProfileUnavailableException("downstream calls exceeded 2s budget", e);
} catch (InterruptedException | ExecutionException e) {
Thread.currentThread().interrupt();
throw new ProfileUnavailableException("failed to aggregate profile", e);
}
return assembleProfile(userFuture.join(), ordersFuture.join(), prefsFuture.join());
}
private AggregatedProfile assembleProfile(UserDto user, List<OrderDto> orders, PreferencesDto prefs) {
AggregatedProfile profile = new AggregatedProfile();
profile.setUser(user);
profile.setOrders(orders);
profile.setPreferences(prefs);
profile.setOrderCount(orders.size());
return profile;
}
}
@RestController
@RequestMapping("/api/profiles")
public class ProfileController {
private final ProfileAggregationService aggregationService;
public ProfileController(ProfileAggregationService aggregationService) {
this.aggregationService = aggregationService;
}
@GetMapping("/{userId}")
public ResponseEntity<AggregatedProfile> getProfile(@PathVariable long userId) {
return ResponseEntity.ok(aggregationService.buildProfile(userId));
}
@ExceptionHandler(ProfileUnavailableException.class)
public ResponseEntity<String> handleUnavailable(ProfileUnavailableException ex) {
return ResponseEntity.status(HttpStatus.GATEWAY_TIMEOUT).body(ex.getMessage());
}
}
This snippet shows how a Spring Boot service fans out to several independent downstream systems in parallel and joins their results into one aggregated response, instead of calling them one after another. The pattern matters whenever a single endpoint has to stitch together data from multiple microservices — a naive sequential implementation pays the sum of every latency, while a parallel one pays roughly the slowest single call.
In AsyncConfig, a dedicated ThreadPoolTaskExecutor bean named downstreamExecutor is registered and @EnableAsync is switched on. Giving the async work its own bounded pool is deliberate: it isolates outbound I/O from the Tomcat request threads, and setThreadNamePrefix makes the stacks readable in logs. Relying on the auto-configured SimpleAsyncTaskExecutor is avoided because it spawns an unbounded number of threads.
In ProfileClient, each downstream call is wrapped in an @Async("downstreamExecutor") method that returns CompletableFuture<T>. The key subtlety is that the method itself calls the blocking RestTemplate and then wraps the value with CompletableFuture.completedFuture(...); because the method is proxied as @Async, Spring runs the whole body on the executor and hands back a real, already-executing future. A brittle exceptionally fallback lets one flaky dependency degrade to an empty value rather than fail the entire aggregation, which is the common trade-off between completeness and availability.
In ProfileAggregationService, the three futures are kicked off up front so they run concurrently, then CompletableFuture.allOf(...) waits for all of them. Using .get(2, TimeUnit.SECONDS) on the combined future enforces a single overall budget; on TimeoutException the code cancels outstanding work and throws a domain exception. After the join, each individual .join() is non-blocking because completion is guaranteed. assembleProfile merges the parts into the response DTO.
In ProfileController, the endpoint simply delegates and returns the assembled DTO. A pitfall worth noting: @Async only works through the Spring proxy, so the client methods must be invoked from a different bean (as they are here) rather than via a self-call, which would bypass the proxy and run synchronously. Reaching for this pattern makes sense when downstream calls are independent and the aggregate latency, not throughput, is the bottleneck.
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
use crossbeam::channel::unbounded;
use std::thread;
fn main() {
let (tx, rx) = unbounded();
Crossbeam for advanced concurrent data structures
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
Channels (mpsc) for message passing between threads
export type Settled<R> =
| { status: 'fulfilled'; value: R }
| { status: 'rejected'; reason: unknown };
export interface ConcurrencyOptions {
limit: number;
Simple concurrency limiter for batch operations
Share this code
Here's the card — post it anywhere.