Key-value & sub-document operations

This section documents the current Couchbase Server 7.6.x line as published at the Couchbase Server documentation, which is the reference these pages are written and verified against. No specific patch version is pinned. Some capabilities (Enterprise-Edition-only Analytics, auditing, encryption at rest, the Backup service and rack-zone awareness, and Capella-only App Services and Columnar) are linked, not documented in depth.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, as Couchbase iterates quickly.

This section’s bibliography lists the reference material consulted while preparing these pages.

The key-value (KV) API is the fastest way in and out of Couchbase: it addresses one item by key and skips the query engine entirely. This page covers the connection path, the CRUD verbs, the atomic numeric and binary operations, and the sub-document API that reads or changes part of a document in a single round-trip.

Connecting: Cluster to Collection

Access always narrows through the same chain: a Cluster (a connection to the whole deployment) yields a Bucket, which yields a Scope, which yields a Collection. The default collection (_default scope, _default collection) exists in every bucket for backward compatibility.

Cluster cluster = Cluster.connect("couchbase://127.0.0.1", "appuser", "s3cr3t");
Bucket bucket = cluster.bucket("travel-sample");
bucket.waitUntilReady(Duration.ofSeconds(10));
Scope scope = bucket.scope("inventory");
Collection collection = scope.collection("airline");
# https://docs.couchbase.com/java-sdk/current/howtos/kv-operations.html

The CRUD verbs

Operation Behavior

insert(id, value)

Create only. Fails with DocumentExistsException if the key already exists.

upsert(id, value)

Create or overwrite unconditionally.

replace(id, value)

Overwrite only. Fails with DocumentNotFoundException if the key is absent. Accepts a CAS for optimistic locking.

get(id)

Fetch the value and metadata. Fails with DocumentNotFoundException if absent.

remove(id)

Delete. Fails with DocumentNotFoundException if absent. Accepts a CAS.

exists(id)

Cheap existence check; returns a result whose exists() is a boolean, never throws for "not found".

getAndTouch(id, ttl)

Fetch the value and reset the expiry in one operation.

try {
    collection.insert("airline_1010", JsonObject.create().put("name", "40-Mile Air").put("iata", "Q5"));
} catch (DocumentExistsException e) {
    // key already present -- fall back to replace-with-CAS or upsert
}

GetResult r = collection.get("airline_1010");
JsonObject air = r.contentAsObject();
long cas = r.cas();

collection.replace("airline_1010", air.put("country", "United States"),
    ReplaceOptions.replaceOptions().cas(cas));   // fails with CasMismatchException if changed meanwhile

boolean present = collection.exists("airline_9999").exists();   // false, no exception
collection.remove("airline_1010");

Every operation takes an options object for timeout, durability, expiry, and CAS. Errors are typed: catch DocumentNotFoundException, DocumentExistsException, CasMismatchException, DurabilityImpossibleException, TimeoutException rather than inspecting status codes. See KV operations for the full result and error model, and Data Service fundamentals for how a KV request is routed.

Atomic counters, binary append/prepend, range scan

increment / decrement atomically adjust an integer document held as an ASCII number. Both accept an initial value used only when the key does not yet exist, and a delta:

# Server-side atomic add; no read-modify-write race between clients
CounterResult c = collection.binary().increment("counter:visits",
    IncrementOptions.incrementOptions().initial(0).delta(1));
long current = c.content();

collection.binary().decrement("counter:seats-left",
    DecrementOptions.decrementOptions().initial(180).delta(2));
# https://docs.couchbase.com/java-sdk/current/howtos/kv-operations.html

append / prepend add bytes to the end or start of a non-JSON (binary/string) value. They must not be used on JSON documents — the result would not be valid JSON. For structured edits use the sub-document API below.

KV Range Scan (7.6) streams every key, or every key in a prefix, directly from the data service without an index. It is meant for background/export jobs, not low-latency request paths:

# Stream all keys beginning with "airline_" from the collection
ScanResult scan = collection.scan(ScanType.prefixScan("airline_"),
    ScanOptions.scanOptions().idsOnly(true));
scan.forEach(item -> process(item.id()));
# https://docs.couchbase.com/java-sdk/current/howtos/kv-operations.html

For throughput, the SDKs expose async (CompletableFuture) and reactive (Mono / Flux) variants of every KV call via collection.async() and collection.reactive(). A "bulk" operation is simply many async calls issued together and awaited as a batch — there is no separate multi-get wire command:

List<CompletableFuture<GetResult>> futures = keys.stream()
    .map(k -> collection.async().get(k))
    .collect(Collectors.toList());
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();

The sub-document API

lookupIn (read) and mutateIn (write) operate on paths inside a JSON document, so the server sends or receives only the fragment involved instead of the whole value. This cuts bandwidth and, for mutateIn, removes the fetch-modify-store race: the change is applied server-side in one atomic step, optionally guarded by CAS.

Per-path operations:

Spec Effect

get / exists / count

Read a value at a path, test a path, or count an array/object at a path.

upsert / insert / replace / remove

Set, add-only, overwrite-only, or delete a field at a path.

arrayAppend / arrayPrepend / arrayInsert

Add elements at the end, start, or a given index of an array.

arrayAddUnique

Append a scalar only if it is not already present in the array.

increment / decrement

Atomically adjust a numeric field (sub-document counter).

# Read three fields (and one XATTR) without transferring the whole document
LookupInResult look = collection.lookupIn("airline_10", Arrays.asList(
    LookupInSpec.get("name"),
    LookupInSpec.get("country"),
    LookupInSpec.exists("icao"),
    LookupInSpec.get("$document.exptime").xattr()));   # virtual XATTR: server-computed expiry
String name = look.contentAs(0, String.class);
boolean hasIcao = look.exists(2);

# Mutate several paths in one atomic round-trip
collection.mutateIn("airline_10", Arrays.asList(
    MutateInSpec.upsert("updatedAt", Instant.now().toString()),
    MutateInSpec.arrayAddUnique("routes", "SFO-JFK"),
    MutateInSpec.increment("version", 1),
    MutateInSpec.upsert("audit.by", "svc-import").xattr().createPath()),
    MutateInOptions.mutateInOptions().cas(knownCas));
# https://docs.couchbase.com/java-sdk/current/howtos/subdocument-operations.html

createPath tells a mutation to create any missing intermediate objects along the path (so audit.by works even when audit did not exist). Adding .xattr() targets an Extended Attribute instead of the document body. A mutateIn can address at most 16 paths per call.

Why it matters: fetching a 200 KB document to change one timestamp and writing it back moves 400 KB over the wire and can lose a concurrent update. The equivalent mutateIn moves a few hundred bytes and cannot clobber fields it does not name. See Sub-Document operations for path syntax, macro values, and the per-path error model.

Contrast with MongoDB

Couchbase’s mutateIn path operations play the role MongoDB’s update operators play: both change part of a document server-side without a client round-trip for the read. The main differences are that Couchbase addresses exactly one document by key (no query filter selects the target), a single mutateIn batches heterogeneous path ops with an optional CAS guard, and array de-duplication is an explicit arrayAddUnique rather than $addToSet. For the operator-update model and multi-document update semantics, see MongoDB Updating & Deleting.