The Reactive Couchbase SDK is actually slower for simple, blocking operations than the classic SDK, making it the wrong tool for many common use cases.
Let’s see it in action. Imagine we’re building a simple user profile service.
First, the classic SDK approach. This is what you’re probably used to. We fetch a user document, update a field, and save it back.
// Classic SDK example
Cluster cluster = Cluster.connect("couchbase://localhost", "user", "password");
Bucket bucket = cluster.bucket("myBucket");
Collection collection = bucket.defaultCollection();
// Get the document
GetResult getResult = collection.get("user:123");
JsonDocument document = JsonDocument.create(getResult.id(), getResult.content());
// Update a field
document.content().put("lastLogin", LocalDateTime.now());
// Replace the document
collection.replace(document);
cluster.disconnect();
This is straightforward. We make a request, wait for the response, process it, and make another request. Each step blocks the thread until it’s done.
Now, the reactive SDK. It uses Project Reactor, so we’re dealing with Mono and Flux.
// Reactive SDK example
Cluster cluster = Cluster.connect("couchbase://localhost", "user", "password");
Bucket bucket = cluster.bucket("myBucket");
Collection collection = bucket.defaultCollection();
// Get the document
Mono<GetResult> getMono = collection.get("user:123");
// Update a field and replace
getMono.flatMap(getResult -> {
JsonDocument document = JsonDocument.create(getResult.id(), getResult.content());
document.content().put("lastLogin", LocalDateTime.now());
return collection.replace(document);
})
.subscribe(replaceResult -> {
System.out.println("User updated: " + replaceResult.id());
}, error -> {
System.err.println("Error: " + error.getMessage());
});
// Note: In a real app, you'd manage the lifecycle and potentially block here
// for demonstration purposes, or integrate with a reactive web framework.
// For this example, we might need a latch to wait for completion.
// CountDownLatch latch = new CountDownLatch(1);
// ... subscribe(..., error -> { latch.countDown(); }, ...)
// latch.await();
cluster.disconnect();
Notice how we don’t explicitly wait. The subscribe call kicks off the process, and the flatMap chains operations. The underlying thread isn’t stuck waiting for I/O; it’s free to do other work. This is the core idea of reactive programming: non-blocking, event-driven, and composable asynchronous operations.
The problem this solves is efficiently handling many concurrent operations without tying up threads. In a high-throughput web service, you might have thousands of requests coming in simultaneously. With a classic, thread-per-request model, you’d need thousands of threads, which is resource-intensive. The reactive approach uses a small pool of threads to manage many I/O operations concurrently.
Internally, the reactive SDK leverages Netty, a high-performance asynchronous event-driven network application framework. When you call collection.get(), it doesn’t block the thread. Instead, it initiates the network request and registers a callback. When the response comes back from Couchbase, Netty (via the SDK) triggers the callback, and your reactive stream (Mono or Flux) continues to process the result. This is why it excels under heavy load.
The exact levers you control are the reactive operators: map, flatMap, filter, zip, merge, and so on. These allow you to compose complex asynchronous workflows from simple building blocks. For example, fetching a user, then fetching their recent orders, and then combining that data into a single response can be expressed elegantly with zip or flatMap.
Most people don’t realize that while the reactive SDK is designed for concurrency, the overhead of setting up the reactive pipeline (creating the Mono/Flux, managing subscriptions) actually makes it slower for single, isolated operations compared to the classic SDK’s direct, blocking calls. If you’re just fetching one document in a background task where blocking is acceptable, the classic SDK will likely perform better due to less ceremony.
The next concept you’ll grapple with is error handling within complex reactive pipelines, especially when dealing with retries and circuit breakers.