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.
Next in Java & Spring Practical Labs: Validated create endpoint
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.Spot a weak answer, missing edge case, or clearer explanation? Send it in.
Say thanks with a standalone, one-time $5 contribution. No account required.