Around Go: MongoDB and Redis · 3 MIN READ

MongoDB: put the invariant in the write predicate

Prevent lost updates with atomic operators and optimistic version checks.

Reading a document, changing a field in Go and writing it back leaves a window for another request to change the same value. Memory race detection cannot find that distributed lost update. Express the allowed transition in the database predicate and inspect whether the write actually matched.

Around Go: data stores · A MAP OF THE IDEA

  1. Expected version
  2. Conditional update
  3. Matched result
  4. Conflict or success

Useful when

  • Prevent lost updates with atomic operators and optimistic version checks.

Watch out

  • No driver error does not mean the predicate matched. Check MatchedCount and handle an absent or stale-version document; one-document atomicity does not cover every business invariant.

Implementation & operation cost: Version conflicts become visible application outcomes; the caller needs retry, merge or user-facing conflict behavior.

01

Update fields without replacing unrelated state

Use an atomic field operator when it expresses the intended operation. A client-side read followed by replacement can lose a concurrent edit or erase fields the caller never loaded. Include ownership predicates such as the tenant in the write, not only in the earlier read.

go
filter := bson.D{{Key: "_id", Value: id}, {Key: "tenant", Value: tenant}, {Key: "version", Value: expected}}
update := bson.D{
    {Key: "$set", Value: bson.D{{Key: "body", Value: body}}},
    {Key: "$inc", Value: bson.D{{Key: "version", Value: 1}}},
}
result, err := notes.UpdateOne(ctx, filter, update)
if err != nil { return err }
if result.MatchedCount == 0 { return ErrConflict }
Read the code

This fragment assumes the shared v2 driver collection and application variables. A zero match can mean stale version or absence; map that to the application contract rather than reporting an unconditional successful save.

02

Enforce uniqueness where concurrent writers meet

A preliminary find cannot guarantee that another request will not insert the same business key. Define a unique index for that invariant and handle duplicate-key errors deliberately. In multi-tenant systems, decide whether uniqueness is global or scoped to a tenant.

javascript
db.notes.createIndex({tenant: 1, externalID: 1}, {unique: true})
db.notes.updateOne(
  {tenant: "demo", externalID: "invoice-42"},
  {$setOnInsert: {body: "first value", version: 1}},
  {upsert: true}
)
Read the code

The example is a development upsert keyed by a stable external identifier. Review existing duplicate data before adding the unique index; deployment of an invariant is itself a migration that can fail.

03

Single-document atomicity has a boundary

One atomic update does not make a two-document business operation atomic. Prefer a document model that contains the invariant when practical, or use a transaction with a clear cost and retry policy. Avoid promising exactly-once business effects just because the driver can retry an eligible write.

TAKE THIS WITH YOU

Use conditional atomic writes and database-enforced uniqueness for concurrent invariants. Treat unmatched writes and duplicate keys as meaningful outcomes.

Check your understanding

What must a version-checked update inspect after no driver error?

Go to the source

Progress stays in this browser. No account needed.