SDKs
Java async support: Native non-blocking SDKs with reactive streaming
Vishal Gowda
September 3, 2025 - 6 min read
Java SDKs, now async‑native: CompletableFuture + Reactive Streams
Java’s ecosystem has steadily shifted toward asynchronous, event‑driven programming. Reactive architectures are increasingly adopted by major platforms1, while CompletableFuture is ubiquitous in modern APIs. SDKs should match this async‑first reality—without forcing teams to abandon familiar synchronous code.
What’s new
- Async native:
CompletableFuture<T>returned by standard methods. - Streaming built‑in: Reactive Streams
Publisher<T>for pagination, SSE, JSONL, and file I/O. - Dual SDK: Synchronous by default; opt in with
.async()per call‑site or service. - Blob: A light, framework‑agnostic byte‑stream abstraction for efficient uploads/downloads.
Why async
Traditional blocking SDKs dedicate one thread per in‑flight request. That results in idle threads, wasted memory, and hard‑to‑tune pools. Especially under high concurrency and variable network latency. Async I/O scales with a small, fixed number of threads without sacrificing composition2 or backpressure 3. For key reactive concepts, see the Reactive Manifesto glossary .
Async I/O is already the standard everywhere else
Python has asyncio, JavaScript has async/await with Promise, Go has goroutines, and Swift has async/await. Async I/O is the de facto model across modern languages. Java’s ecosystem is rapidly moving in this direction—think of this as Python’s asyncio but for Java, with the added benefits of strong typing and mature reactive ecosystems.
When async provides the most value
SDK design insight for API producers
Your developers have diverse needs: some build high-concurrency microservices that benefit from async patterns, while others prefer straightforward synchronous flows. Don’t make them choose at the SDK level—cater to both paradigms in one unified SDK so they can adopt async where it adds value without abandoning familiar synchronous code. The most successful SDKs optimize for developer choice, not architectural dogma.
Here’s how this works in practice. We expose both sync and async interfaces from one SDK:
// Build once — defaults to synchronous behavior
TradingSDK sdk = TradingSDK.builder()
.serverURL("https://api.trading.com")
.apiKey("your-api-key")
.build();
// Synchronous usage (existing behavior)
Portfolio portfolio = sdk.getPortfolio("user-123");
List<Trade> trades = sdk.getTrades(portfolio.getId());
// Asynchronous usage via .async()
CompletableFuture<List<Trade>> asyncTrades = sdk
.async()
.getPortfolio("user-123")
.thenCompose(p -> sdk.async().getTrades(p.getId()));Backward compatibility
The builder yields a synchronous SDK by default, so no breaking changes. Opt into async via .async(); the mode applies consistently across sub‑SDKs.
Implementation deep‑dive
Our HTTP stack uses Java 11’s HttpClient async APIs and NIO.2 primitives (ByteBuffer, AsynchronousFileChannel, AsynchronousSocketChannel) for end‑to‑end non‑blocking I/O.
// The underlying HTTP client call
client.sendAsync(
request,
HttpResponse.BodyHandlers.ofPublisher());
// returns a CompletableFutureOur SDKs go to lengths to ensure there is as little as possible thread blocking work. For standard JSON responses, we collect and decode bytes asynchronously using thenApply/thenCompose. For streams, we hook into Flow.Publisher<List<ByteBuffer>>.
Async iterables via Reactive Streams
For async iterables (pagination, streaming responses), we represent them as Reactive Streams Publisher<T>. Traditional Java collections or custom iterators aren’t feasible for async scenarios—they can’t express non‑blocking backpressure or handle variable consumption rates gracefully. Using Publisher<T> is an increasingly common idiom in modern Java applications, providing seamless interoperability with mature reactive ecosystems like Project Reactor , RxJava , Akka Streams , Vert.x , and Quarkus Mutiny . We keep dependencies light by implementing JDK‑native operators (map, mapAsync, concat, wrap, flatten) through custom publishers, subscribers, and subscriptions.
Protocol‑aware streaming
- SSE & JSONL: Custom publishers bridge raw byte streams to protocol parsers, yielding typed events on backpressure‑aware
Publisher<T> - Pagination: A generic, non‑blocking pagination publisher drives page fetches using pluggable
ProgressTrackerStrategyto parse cursors and termination conditions
Retries, timeouts, and hooks
- Async retries use
ScheduledExecutorServicefor non‑blocking exponential backoff with jitter - Timeouts/cancellation4 are surfaced with
CompletableFuture#orTimeout,completeExceptionally, and cancellation propagation to HTTP requests - Hook transformations leverage
CompletableFuturefor zero‑blocking customization points
Efficient payloads with Blob
Blob is our core abstraction for working with streams of ByteBuffer:
// Factories
Blob.from(Paths.get("large-dataset.json")); // File path
Blob.from(inputStream); // InputStream
Blob.from("text content"); // String
Blob.from(byteArray); // byte[]
Blob.from(flowPublisherOfByteBufferLists);
// Flow.Publisher<List<ByteBuffer>>
// Consumption
blob.asPublisher(); // org.reactivestreams.Publisher<ByteBuffer>
blob.toByteArray(); // CompletableFuture<byte[]>
blob.toFile(targetPath); // CompletableFuture<Path>
blob.toInputStream(); // InputStream bridgeMultipart uploads concatenate publishers—each part (form field or file) is its own stream. Downloads expose Blob so you can stream to disk without buffering whole payloads.
Streaming uploads for synchronous SDKs
Enable enableStreamingUploads: true to get the same memory-efficient upload capabilities
// Sync SDK with streaming uploads
syncSDK.uploadDataset(Blob.from(
Paths.get("./datasets/large-file.jsonl")));// Stream a large model to disk
CompletableFuture<Path> model = asyncMLSDK.downloadModel("bert-large.bin")
.thenCompose(res -> res.body().toFile(Paths.get("./models/bert-large.bin")));
// Stream binary telemetry (JSONL/SSE similar)
var bytes = asyncIoTSDK.streamSensorData("factory-floor-1")
.thenApply(res -> res.body().asPublisher());
// Use your favourite reactive libraries to react to stream
Flux.from(Mono.fromFuture(bytes))
.flatMap(Flux::from)
.buffer(1024)
.map(this::parseBinaryTelemetry)
.filter(r -> r.getTemperature() > CRITICAL_THRESHOLD)
.subscribe(this::triggerAlert);
// Upload a 10GB dataset without memory pressure
CompletableFuture<UploadResponse> up = asyncDataSDK.uploadDataset(
Blob.from(Paths.get("./datasets/customer-data-10gb.jsonl"))
);Reactive Streams vs JDK Flow
We expose Reactive Streams types in the public API for ergonomic use with the wider ecosystem. Java 11+ HTTP APIs expose Flow.Publisher under the hood; we convert via FlowAdapters and flatten List<ByteBuffer> as needed. If you require Flow.Publisher, adapters are available in both directions.
Reactive streaming patterns
Reactor
// Paginate users
Publisher<User> users = asyncCRMSDK.listUsers().asPublisher();
Flux.from(users)
.filter(User::isActive)
.flatMap(u -> sendWelcomeEmail(u).thenReturn(u))
.subscribe();RxJava
// Real-time logs (SSE/JSONL abstracted)
Publisher<LogEntry> logStream = asyncLoggingSDK.streamLogs();
Flowable.fromPublisher(logStream)
.filter(log -> "ERROR".equals(log.getLevel()))
.window(30, TimeUnit.SECONDS)
.flatMapSingle(window -> window.toList())
.subscribe(alertingService::sendAlert);Akka Streams
// Batched notifications
Publisher<Notification> notifications = asyncNotificationSDK.streamNotifications();
Source.fromPublisher(notifications)
.filter(n -> "HIGH".equals(n.getPriority()))
.mapAsync(10, this::enrichWithUserData)
.grouped(50)
.runForeach(batch -> pushNotificationService.sendBulk(batch), system);On Virtual Threads
Virtual threads make synchronous code scale far better by reducing the cost of blocking. They’re a great fit for simple request/response flows and can be used with the synchronous SDK today. Our async SDK still adds value where you need backpressure, streaming, cancellation, and composition across multiple concurrent I/O operations—patterns that CompletableFuture/Publisher express naturally.
For API Producers: the unified paradigm advantage
- Broader reach: Works for Spring MVC (sync) and WebFlux/Quarkus (async)
- Migration flexibility: Adopt async gradually without replacing integrations
- Lower maintenance: One codebase; two clean interfaces
- Consistent DX: Same auth, errors, and config everywhere
Getting started
Enable the following flag in your .speakeasy/gen.yaml:
java:
asyncMode: enabledThen run speakeasy run to regenerate your SDK.
For existing SDKs, see our migration guide for step-by-step instructions on enabling async support for your SDKs.
Looking ahead
This release lays the groundwork for the full spectrum of modern Java patterns—from cloud‑native microservices to real‑time streaming systems. Ready to unlock the benefits of non‑blocking I/O? Regenerate your Java SDK and try it today.
Footnotes
-
Netflix API with RxJava | The Rise of Reactive Programming in Java | Java Reactive Programming: An In-Depth Analysis | Reactive Programming is Not a Trend | Java Reactive Programming ↩
-
Composition — The ability to chain and combine async operations declaratively, like
future.thenCompose()or stream operators, without manual thread coordination. ↩ -
Spring WebFlux Overview — Why reactive/non‑blocking scales with a small, fixed number of threads. ↩
-
Cancellation — The ability to interrupt or stop async operations gracefully, propagating cancellation signals through composed operations to free resources and avoid unnecessary work. ↩