Interview question
Run bounded concurrent HTTP fetches in Java
Fetches multiple URLs with bounded concurrency, per-request and batch deadlines, cancellation, ordered results, and executor cleanup.
TL;DR
Fetches multiple URLs with bounded concurrency, per-request and batch deadlines, cancellation, ordered results, and executor cleanup.
ExecutorService, HttpClient, deadlines, interruption, cancellation, ordering, resource ownership, and partial failure.
Practice the problem like a real interview: restate, reason, implement, and test.
Implement a batch fetcher for at most 100 URLs. It must run no more than maxConcurrency blocking requests at once, preserve input order in the returned results, enforce both a per-request timeout and an overall batch timeout, cancel unfinished work, and release its executor.
3 URLs, maxConcurrency=2 -> at most 2 active requests
responses complete C, A, B -> results are returned A, B, C
overall timeout expires -> unfinished entries are reported as timed out
maxConcurrency from 1 to 16.Callable<FetchResult> per input URL with an HttpRequest timeout.invokeAll; its returned futures keep task-list order and unfinished tasks are cancelled.shutdownNow in finally; interruption must also restore the thread flag.import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
public final class BatchFetcher {
public record FetchResult(URI uri, Integer status, String body, String error) {}
public static List<FetchResult> fetch(
List<URI> uris, int maxConcurrency,
Duration requestTimeout, Duration batchTimeout) {
if (uris.isEmpty() || uris.size() > 100) throw new IllegalArgumentException("uris");
if (maxConcurrency < 1 || maxConcurrency > 16) throw new IllegalArgumentException("maxConcurrency");
var client = HttpClient.newHttpClient();
var pool = Executors.newFixedThreadPool(maxConcurrency);
try {
List<Callable<FetchResult>> tasks = uris.stream().map(uri ->
(Callable<FetchResult>) () -> {
var request = HttpRequest.newBuilder(uri).timeout(requestTimeout).GET().build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
return new FetchResult(uri, response.statusCode(), response.body(), null);
}).toList();
var futures = pool.invokeAll(tasks, batchTimeout.toMillis(), TimeUnit.MILLISECONDS);
var results = new ArrayList<FetchResult>(uris.size());
for (int i = 0; i < futures.size(); i++) {
try {
results.add(futures.get(i).get());
} catch (CancellationException e) {
results.add(new FetchResult(uris.get(i), null, null, "timed out"));
} catch (ExecutionException e) {
results.add(new FetchResult(uris.get(i), null, null,
e.getCause().getClass().getSimpleName()));
}
}
return List.copyOf(results);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("batch interrupted", e);
} finally {
pool.shutdownNow();
}
}
}
Task setup and result assembly are O(n). At most maxConcurrency blocking requests execute at once, while O(n) tasks and results are retained for the batch.
InterruptedException breaks cooperative cancellation.Move to the linked follow-up, next path step, prerequisite, or deeper variant.
Answer a similar question in a different context.