Prompt 18 · Lesson 03 · Event payload semantics
Full Document/Pre-Image Options, Update Descriptions, Deletes, and Schema Evolution
An update delta, a current lookup, and retained pre/post images answer different questions. Choose the payload according to the consumer invariant.
Learning objectives
Distinguish updateDescription deltas, updateLookup current-document lookup, stored post-images, and pre-images.
Enable collection pre/post images and choose whenAvailable versus required according to failure tolerance.
Handle delete events where the current document no longer exists.
Build schema-tolerant consumers that accept update and replace events and version their downstream contract.
Account for pre-image storage/retention and the 16 MiB change-event BSON limit.
This lesson pins MongoDB Community Server
8.3.8 with
mongodb/mongodb-community-server:8.3.8-ubuntu2204-slim, mongosh 2.10.0, and PyMongo 4.17.0.
The mandatory lab uses a disposable
single-member replica set on loopback port
27157; change streams require a replica set or sharded cluster,
so a standalone is intentionally not used. A single-member
replica set is enough to learn event/resume mechanics but does
not demonstrate high availability, multi-node majority
durability, or failover. Authentication and TLS are disabled
only for this isolated lab. Feature Compatibility Version (FCV)
is inspected. FCV is observed and never changed. Default
read/write concern and primary read preference apply unless a
command says otherwise. The collection enables
changeStreamPreAndPostImages. Pre-images are stored
in config.system.preimages and add
storage/processing cost; the lesson inspects but never directly
mutates that internal collection. Atlas, Search, Vector Search,
KMS, and Enterprise Advanced are not mandatory. Product commands
were not executed in this generation environment because Docker,
mongod, mongosh, and PyMongo are unavailable here; runtime
timings and token values must be measured on the learner machine
rather than copied as invented output.
docker rm -f atlasmart-ch18-l3 2>/dev/null || truedocker volume rm atlasmart-ch18-l3-data 2>/dev/null || truedocker run -d --name atlasmart-ch18-l3 \ -p 127.0.0.1:27157:27017 \ -v atlasmart-ch18-l3-data:/data/db \ mongodb/mongodb-community-server:8.3.8-ubuntu2204-slim --replSet atlasmart-rs18-l3 --oplogSize 128 --bind_ip_alluntil mongosh "mongodb://127.0.0.1:27157/admin?directConnection=true" --quiet --eval 'quit(db.runCommand({ping:1}).ok===1?0:1)'; do sleep 1; donemongosh "mongodb://127.0.0.1:27157/admin?directConnection=true" --quiet --eval 'rs.initiate({_id:"atlasmart-rs18-l3",members:[{_id:0,host:"atlasmart-ch18-l3:27017"}]})'until mongosh "mongodb://127.0.0.1:27157/admin?directConnection=true" --quiet --eval 'quit(db.hello().isWritablePrimary?0:1)'; do sleep 1; donemongosh "mongodb://127.0.0.1:27157/admin?replicaSet=atlasmart-rs18-l3" --quiet --eval 'printjson(db.version());printjson(db.runCommand({getParameter:1,featureCompatibilityVersion:1}).featureCompatibilityVersion);printjson(rs.status().members.map(m=>({name:m.name,stateStr:m.stateStr})));'
1. Four payload ideas that are easy to conflate
For an update,
updateDescription describes the changed/removed
fields. fullDocument:"updateLookup" performs a
later lookup of the current majority-committed document;
intervening writes can make that lookup newer than the event's
delta. MongoDB 6.0+ pre/post images instead preserve document
images associated with the change when the feature was enabled
and the image remains retained. A delete has no current
post-image because the document no longer exists.
| Payload | Meaning | Important edge |
|---|---|---|
updateDescription |
Delta for the update event. | Consumer must understand paths/array truncation and tolerate schema evolution. |
fullDocument:"updateLookup" |
Current majority-committed document when lookup executes. | May include later changes and may be missing if document/namespace changed. |
fullDocument:"whenAvailable" |
Stored post-image when available. | Requires pre/post-image feature for update post-image semantics. |
fullDocumentBeforeChange |
Stored pre-image before update/replace/delete. | Can be unavailable because feature was off or retention removed it. |
2. Enable images and capture update + delete evidence
const app=db.getSiblingDB("atlasmart");app.products_ch18_l3.drop();app.createCollection("products_ch18_l3", {changeStreamPreAndPostImages:{enabled:true}});app.products_ch18_l3.insertOne({_id:"p-3",schemaVersion:1,name:"Router",price:99,tags:["network"]});printjson(app.getCollectionInfos({name:"products_ch18_l3"})[0].options);printjson(db.getSiblingDB("config").system.preimages.stats());
from pprint import pprintfrom pymongo import MongoClientclient = MongoClient("mongodb://127.0.0.1:27157/?replicaSet=atlasmart-rs18-l3")c = client.atlasmart.products_ch18_l3with c.watch( full_document="whenAvailable", full_document_before_change="whenAvailable", max_await_time_ms=1000,) as stream: c.update_one({"_id":"p-3"},{"$set":{"schemaVersion":2,"price":89,"attributes":{"wifi":"6E"}}}) update_event = stream.next() pprint({ "op": update_event["operationType"], "delta": update_event.get("updateDescription"), "before": update_event.get("fullDocumentBeforeChange"), "after": update_event.get("fullDocument"), }) c.delete_one({"_id":"p-3"}) delete_event = stream.next() pprint({ "op": delete_event["operationType"], "documentKey": delete_event["documentKey"], "before": delete_event.get("fullDocumentBeforeChange"), "after": delete_event.get("fullDocument"), })client.close()
Expected invariant: the update has a delta plus available
pre/post image; the delete retains documentKey and
can have a pre-image, but there is no post-delete document.
Exact resume tokens and timestamps are runtime values.
3. required versus whenAvailable is a correctness decision
whenAvailable lets the stream continue with a
null/missing image if the image is unavailable.
required asks the server to fail when the requested
image cannot be returned. Use required only when
missing an image is itself a correctness failure and the
retention/storage design supports that contract. Pre-images are
removed asynchronously and also disappear when their
corresponding change event falls out of the oplog, regardless of
a longer configured image retention period.
A change event is a BSON document and must remain under MongoDB's 16 MiB BSON document limit. Asking simultaneously for a large pre-image, post-image, and a large updateDescription can exceed that limit. Keep watched documents bounded and request only the payload needed by the consumer.
4. Deliberately wrong consumer: “all changes are update events with fullDocument”
MongoDB may represent a replacement as
replace rather than update, and delete
events have no current document. A consumer that assumes
operationType=="update" and blindly dereferences
fullDocument will silently miss replacements or
crash on deletes. Normalize operations into an application-owned
event envelope and keep schema versioning explicit.
def normalize(change): op = change["operationType"] if op in {"insert", "replace", "update"}: doc = change.get("fullDocument") return { "kind": "product-upsert", "sourceOp": op, "id": change["documentKey"]["_id"], "schemaVersion": (doc or {}).get("schemaVersion"), "document": doc, "delta": change.get("updateDescription"), } if op == "delete": return { "kind": "product-delete", "sourceOp": op, "id": change["documentKey"]["_id"], "before": change.get("fullDocumentBeforeChange"), } return {"kind":"control-or-unknown", "sourceOp":op}
5. Production judgment
Use deltas when downstream state can apply patches safely; use
post-images when consumers need a full snapshot; use pre-images
for audit/compensation only when the additional storage,
retention, privacy, and event-size costs are justified. Treat
image retention as part of the consumer recovery objective, and
monitor config.system.preimages size. Do not
persist sensitive pre-images indefinitely just because CDC makes
it convenient.
Bridge. Lesson 4 turns the resume token into a durable recovery protocol and shows the special invalidation/history-loss boundaries that ordinary reconnect logic cannot hide.
Cleanup/reset
Everything in this lesson is disposable. Remove only the chapter-specific container and volume:
docker rm -f atlasmart-ch18-l3 2>/dev/null || truedocker volume rm atlasmart-ch18-l3-data 2>/dev/null || true
Check your understanding
- Does updateLookup return the exact document as of the update event?
- Can a delete event have fullDocument after the delete?
- What is the difference between whenAvailable and required?
- Why must consumers handle replace as well as update?
- What operational cost do pre-images add?
Review the answers
1. Not necessarily; it returns the current majority-committed version when the lookup executes, so later writes can be reflected.
2. No current post-image exists after deletion; use documentKey and optionally a retained pre-image.
3. whenAvailable tolerates an unavailable image; required fails when the requested image cannot be returned.
4. MongoDB can represent a replacement as a replace event; filtering only update can miss meaningful changes.
5. Additional storage/processing, retention management, privacy surface, and potentially larger change events.
Authoritative references
- MongoDB Change Streams — deployment requirements, majority-committed notification, scopes, sharded behavior, resume tokens, and pre/post images.
- Change Stream Events — event fields, operation types, resume token, update/replace behavior, and expanded events.
- db.collection.watch() — pipeline stages, options, resumability, and mongosh versus driver behavior.
- db.watch() — database-scoped streams.
- Mongo.watch() — deployment-scoped streams.
- update Event — updateDescription, documentKey, full document and pre-image behavior.
- delete Event — delete event and pre-image behavior.
- invalidate Event — stream invalidation and startAfter boundary.
- Change Streams Production Recommendations — sharded total ordering and latency considerations.
- Privilege Actions — changeStream/find authorization requirements.
- Replica Set Oplog — retained history and oplog window.
- PyMongo Driver — official Python driver baseline and change-stream cursor APIs.
- PyMongo 4.17 Release Notes — current driver line used by the course.
- MongoDB 8.3 Release Notes — current stable minor and patch status.
- MongoDB 8.3 Compatibility Changes — current expanded-event field behavior inherited from 8.2.x.
- mongosh Release Notes — mongosh 2.10.0 baseline.