MongoDB path · Scale and change · Topic 11 of 12

Change streams

Not started

A change stream is a cursor that returns an event for every change to a collection, a database or the whole deployment, in the order the changes were committed. It is built on the oplog, the replication log every replica set already keeps, and exposed through the aggregation framework, so you can filter and reshape events with pipeline stages. It is MongoDB's built-in change data capture.

Why it matters: Change streams let other parts of a system react to data changes (search indexes, caches, events to Kafka) without polling and without the dual-write bug of updating the database and a broker separately.

  1. 1A cursor over the oplog

    watch() on a collection, database or deployment returns events for inserts, updates, replaces and deletes, in commit order.

  2. 2Only majority-committed changes

    Events are sent once a majority has the change, so a consumer never sees a write that is later rolled back.

  3. 3Every event carries a resume token

    Store the token after processing an event; after a restart, resumeAfter (or startAfter) continues from there.

  4. 4The oplog is the window

    You can only resume from a point still in the oplog. A consumer that is down longer than the oplog window must start again from a full copy.

1

How it works

The explanation, with examples you can run.

Watching a collection

const cs = db.orders.watch(  [{ $match: { operationType: { $in: ["insert", "update"] } } }],  { fullDocument: "updateLookup" })while (true) {  const ev = cs.tryNext()  if (ev) printjson(ev); else sleep(200)}

In another shell:

db.orders.insertOne({ _id: "o-1001", customerId: 42, status: "pending", totalCents: 2500 })db.orders.updateOne({ _id: "o-1001" }, { $set: { status: "paid" } })

The watcher printed (trimmed, tokens shortened):

{ "operationType": "insert", "documentKey": { "_id": "o-1001" },  "fullDocument": { "_id": "o-1001", "customerId": 42, "status": "pending", "totalCents": 2500 },  "_id": { "_data": "826AD2214B000000012B042C0100296E5A10..." } }{ "operationType": "update", "documentKey": { "_id": "o-1001" },  "updateDescription": { "updatedFields": { "status": "paid" }, "removedFields": [], "truncatedArrays": [] },  "fullDocument": { "_id": "o-1001", "customerId": 42, "status": "paid", "totalCents": 2500 },  "_id": { "_data": "826AD2214B000000022B042C0100296E5A10..." } }

An update event normally carries only updateDescription, the changed fields. fullDocument: "updateLookup" adds the current document, read when the event is delivered, so it may already include later changes. For the exact before and after state of each change, enable pre- and post-images on the collection (6.0 and later) and use fullDocumentBeforeChange.

Resuming

Each event's _id is its resume token. A consumer that stores the token after processing each event can restart with watch(pipeline, { resumeAfter: token }) and continue exactly after it. That is at-least-once delivery: if the process crashes after acting on an event but before saving its token, it sees the event again, so make the handler idempotent. Do not remove or change _id in your pipeline; the server rejects streams that modify it.

Events are only sent for changes a majority of data-bearing members have, so a consumer never acts on a write that a failover later rolls back.

The oplog window

Resuming only works while the token's position is still in the oplog, which is a capped collection that overwrites old entries. On the test set, db.getReplicationInfo() showed an oplog sized at about 42 GB (by default it takes a share of free disk) but holding only 0.05 hours of history, because the set was new; on a busy production system the window might be hours or days. A consumer down longer than the window gets an error on resume and must rebuild from a full read of the collection. Monitor the window, and size the oplog for your longest expected outage.

Typical uses

Updating a search index or cache, publishing domain events (an outbox: write the order and its event in one transaction, and let a change stream publish the event), and feeding Kafka through the MongoDB Kafka connector.

2

Check yourself

3 questions. Get them all right to finish this topic.

  1. 1.A consumer processes an event, crashes before saving its resume token, and restarts from the previous token. What happens?
  2. 2.An update event has updateDescription but no fullDocument. How do you get the whole document?
  3. 3.Can you open a change stream on a standalone mongod?

0 of 3 right so far. Get all three to finish this topic.