rgoussu@goussu: ~/library/go/storage
~/library/go/storage cat document-and-nosql.md

Document and NoSQL stores from Go

# MongoDB, Cassandra/Scylla, DynamoDB, and Couchbase clients — struct-tag mapping, per-call consistency knobs, and modeling with structs instead of entities.

Conceptsaved 2026-08-09 #go#storage#nosql#mongodb#dynamodb

Overview

There is no unifying abstraction over NoSQL in Go — each store has one canonical client, and each client hands you the store's real semantics: BSON documents, tunable consistency levels, conditional writes. Mapping is done with struct tags (bson, dynamodbav, cql), not managed entities, so a document is just a struct you marshal — no identity map, no lazy loading, no session. The craft is knowing each client's pooling and consistency knobs and modeling documents so that structs stay honest about what is actually stored.

Key points

  • MongoDB — official mongo-go-driver (go.mongodb.org/mongo-driver/v2): ClientDatabaseCollection; every call takes a context.Context. Mapping via bson struct tags (bson:"_id,omitempty"); queries are bson.D/bson.M documents, so filters are data, not a DSL. Change streams (collection.Watch) give ordered, resumable event feeds on replica sets.
  • Mongo consistency surfaces per call: read preference (primary/secondary), read concern, and write concern are options on client, database, collection, or single operation — nothing is hidden behind a template default.
  • Cassandra/ScyllaDB — gocql: session over a token-aware, prepared-statement-cached pool; session.Query("...").Bind(...).Consistency(gocql.Quorum) puts the consistency level in your face on every query. ScyllaDB maintains a fork (github.com/scylladb/gocql) with shard-aware routing — drop-in, worth it on Scylla. scylladb/gocqlx adds struct binding and query building on top.
  • DynamoDB — aws-sdk-go-v2 (service/dynamodb): request/response structs mirror the HTTP API; the feature/dynamodb/attributevalue package marshals Go structs to attribute maps via dynamodbav tags. Expression building (conditions, updates) via the expression package. ConsistentRead: true opts single reads out of eventual consistency; transactions exist (TransactWriteItems) with item-count limits.
  • Couchbase — gocb (and CouchDB via go-kivik): gocb gives KV ops, SQL++ (N1QL) queries, and durability levels per mutation. Brief mentions — reach for the deep docs when needed.
  • Structs, not entities: no dirty checking means updates are explicit operations ($set, update expressions), and partial updates are something you write, not something a framework diffs for you. This kills the read-modify-write-everything anti-pattern early.
  • Schema drift is the tax: old documents missing new fields decode into zero values — design with omitempty, pointers for "absent vs zero", and explicit migration reads.

Details

Client-by-client mechanics

Store Client Mapping Consistency knob location
MongoDB mongo-go-driver bson tags read pref / read+write concern, per op
Cassandra/Scylla gocql (+ scylla fork, gocqlx) positional bind / gocqlx db tags .Consistency(...) per query
DynamoDB aws-sdk-go-v2 + attributevalue dynamodbav tags ConsistentRead, transact APIs
Couchbase gocb json tags durability level per mutation

Patterns that recur

  • Contexts everywhere: all four clients take ctx per operation — timeouts and cancellation are uniform across stores, unlike the JVM's per-driver timeout settings.
  • Conditional writes replace transactions in Dynamo and often Mongo: attribute_not_exists conditions, findOneAndUpdate with filters — model invariants as single-item conditions where possible before reaching for multi-item transactions.
  • Change streams / CDC: Mongo change streams are the built-in outbox alternative; Dynamo has Streams (consumed via Lambda or the Kinesis adapter); Cassandra pushes you to CDC logs.

Examples

// MongoDB: bson tags, context, conditional upsert
type Order struct {
    ID       bson.ObjectID `bson:"_id,omitempty"`
    Customer string        `bson:"customer"`
    Status   string        `bson:"status"`
    Total    int64         `bson:"total_cents"`
}

coll := client.Database("shop").Collection("orders")
_, err := coll.UpdateOne(ctx,
    bson.M{"_id": id, "status": "open"},          // filter is data
    bson.M{"$set": bson.M{"status": "paid"}},     // explicit partial update
)
// DynamoDB: attributevalue marshaling + conditional put
item, _ := attributevalue.MarshalMap(order)
_, err = ddb.PutItem(ctx, &dynamodb.PutItemInput{
    TableName:           aws.String("orders"),
    Item:                item,
    ConditionExpression: aws.String("attribute_not_exists(pk)"), // create-only
})

Related