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.
Move to the linked follow-up, next path step, prerequisite, or deeper variant.
Answer a similar question in a different context.
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.Share the question with a concise Core-answer excerpt and invite other developers to add their perspective.
Help improve the interview library.
Say thanks with a standalone, one-time $5 contribution. No account required.
Aporeon is shaped by Aleksandar Tomovski, a software developer with experience on both sides of technical interviews. Content is reviewed for accuracy, natural spoken delivery, useful depth, and honest trade-offs.