Couchbase Developer common mistakes
Couchbase Developer · Understand the traps and how to avoid them.
Modeling one bucket with a single document type and relying on N1QL WHERE clauses to separate entity types, instead of using document key prefixes and a type discriminator field in every document.
Candidates coming from relational backgrounds think in tables and assume the query engine will filter efficiently by any field. Without a type field, a query like SELECT * FROM bucket WHERE kind = 'user' either fails because the field is absent or forces a full bucket scan, and keys cannot be reasoned about for direct GET access.
What to do instead
Put a mandatory type (or docType) property in every document and encode the entity and access pattern into the key, e.g. user::12345 and order::12345::2024-06. Then write N1QL with WHERE type = 'user' and use USE KEYS or key-range scans for the hottest paths. Verify with EXPLAIN that the query uses the primary index or a secondary index on type rather than a full scan.
Embedding unbounded arrays (order lines, comments, event logs) inside a single parent document and assuming they will stay small.
Embedding is the correct default in Couchbase, so candidates over-apply it without estimating growth. Documents that grow past roughly 20 MB are rejected, and large documents degrade read/write latency and replication because the whole document is rewritten and shipped on every mutation.
What to do instead
Estimate the maximum realistic size and mutation rate. Embed when the child set is bounded and read with the parent; otherwise use a child document with a key that includes the parent id (order::12345::line::0007) and a parent reference. Keep an eye on document size in the data model review and split before approaching the limit.
Writing N1QL with a leading wildcard LIKE '%term%' or a function on an indexed field (WHERE LOWER(email) = $email) and expecting the index to be used.
Candidates assume indexes behave like relational B-tree indexes on any expression. Couchbase secondary indexes are on the indexed key expression and order; a leading wildcard or a function applied at query time cannot use the index ordering and falls back to a scan.
What to do instead
Create a functional index matching the expression, e.g. CREATE INDEX ix_email_lower ON bucket(LOWER(email)), and query with the same expression. For substring search use a full-text search index instead of LIKE. Confirm with EXPLAIN that the plan shows the intended index and no primary scan.
Treating N1QL joins like SQL joins across arbitrary collections and joining on unindexed fields, then blaming the server for slow queries.
N1QL joins are nested-loop joins driven from the left side; the right side must be indexed on the join key or each left row triggers a scan. Candidates also forget that joins in Couchbase are often better replaced by key lookups or denormalized embedding.
What to do instead
Index the right-hand join key, put the more selective and smaller side on the left, and prefer USE KEYS joins when the relationship is by document key. Before writing a join, ask whether the data should be embedded or denormalized to avoid the join entirely; document the decision.
Sharing a single Cluster/SDK connection object per request or creating a new cluster connection inside each handler, rather than creating one long-lived cluster and bucket object at application startup.
Candidates treat the SDK client like a lightweight HTTP client. Cluster creation performs bootstrap, authentication, and topology discovery; doing it per request exhausts connections and adds latency, while a static global without lifecycle management leaks resources on shutdown.
What to do instead
Create the Cluster once at startup, open the bucket and collection handles once, inject them into handlers, and close the cluster on shutdown. Reuse the same objects across threads or coroutines; the SDK is designed to be shared and thread-safe.
Ignoring the result of a mutation and assuming a write succeeded, or catching only one exception type and treating all failures as fatal.
Key-value operations return a result object carrying CAS and mutation token, and failures surface as distinct exceptions (document not found, CAS mismatch, timeout, temporary failure). Candidates who do not check results or who catch a generic exception miss retryable conditions and silently lose writes.
What to do instead
Inspect the mutation result and CAS where relevant, and handle the specific exception classes: treat DocumentNotFoundException as a normal miss, retry on timeout and temporary failure with backoff, and treat CAS mismatch as a concurrency conflict requiring re-read and retry. Log the failure mode explicitly.
Implementing optimistic concurrency incorrectly by reading a document, modifying it, and writing it back without using the CAS value, or by retrying a CAS mismatch with the stale CAS.
Candidates know CAS exists but not the contract: the write must carry the CAS from the read that produced the modified content, and on mismatch the document must be re-read to obtain a fresh CAS before retrying. Reusing the old CAS loops forever.
What to do instead
Use replace with the CAS obtained from the read. On CAS mismatch, re-fetch the document, reapply the mutation, and retry with the new CAS, with a bounded retry count. Alternatively use sub-document mutations for atomic field-level changes that do not require a full read-modify-write.
Describing high availability as 'add a replica and you are safe' without distinguishing replica count, cross-datacenter replication, and failover, or without knowing that failover requires an explicit action and that auto-failover has a configured timeout.
Candidates memorize the feature names but not the operational semantics: replicas protect against node loss only if failover happens, auto-failover is a configured setting with a delay, and XDCR is asynchronous with its own lag. Confusing these leads to wrong recovery expectations.
What to do instead
State the chain explicitly: replicas provide redundant copies, auto-failover (with its timeout) promotes replicas, and rebalance restores balance; XDCR is asynchronous cross-cluster replication for DR and migration, not a substitute for intra-cluster replicas. Verify settings in the cluster UI or CLI rather than assuming defaults.
Assuming memory quota and bucket eviction policy are interchangeable knobs, or setting an eviction policy without understanding what happens to documents when memory fills.
Candidates conflate RAM quota per bucket, the cluster memory quota, and the eviction policy. With value-only eviction, item metadata stays in memory and values are fetched from disk, so the working set matters more than raw document count; choosing no-eviction can cause writes to fail when memory is exhausted.
What to do instead
Size buckets from the working set and document size, keep total bucket quotas under the cluster quota with headroom for indexes and services, and choose the eviction policy deliberately: value-only for cache-like workloads, no-eviction only when the dataset is guaranteed to fit. Monitor eviction and cache-miss metrics.
Building a mobile app that syncs an entire bucket to every device, or designing sync gateway access control that grants broad channels, then discovering data leakage or sync latency.
Candidates port the server-side 'just query it' habit to mobile, where replication is push/pull of documents filtered by channels and the access control function. Broad channels mean every device downloads everything, and misconfigured access grants expose documents across users.
What to do instead
Design channels around the user's data scope (e.g. user::12345), enforce them in the Sync Gateway sync function with a documented rule per document type, and use delta sync and filtered replication to limit payload. Test with two users to confirm each device receives only its own documents.