MongoDB Course — 24 · Kafka + Debezium + MongoDB Kafka Connector (Source + Sink)
The standard way to flow data between MongoDB and Kafka in 2026 is two connectors: MongoDB Kafka Source Connector (Mongo → Kafka, change-stream-based, official) and MongoDB Kafka Sink Connector (Kafka → Mongo). For cross-database CDC (Postgres → Mongo, etc.), Debezium is the de facto. This post covers all three — what each does, when to use which, configurations that work in production, deduplication + ordering, and the common pitfalls (the failed-on-update-events trap, partition keys, schema registry).
Series map: 22 RAG → 23 SQL→Mongo migration → 24 Kafka + Debezium CDC (this post) → 25 Testing → 26 OpenTelemetry → 27 Geo → 28 Time-series extreme → 29 K8s + mongosync + cost.
Learning Outcome
- Set up Kafka + Kafka Connect locally.
- Configure the MongoDB Kafka Source connector with change streams.
- Configure the MongoDB Kafka Sink connector to write to a collection.
- Configure Debezium Postgres connector → Kafka → Mongo sink.
- Handle ordering, deduplication, schema evolution.
- Recognise when to use this stack vs the simpler outbox + change-stream publisher (Post 20).
Chapter 1 — Why Kafka + Connect
Outbox + change streams (Post 20) is the cheap, self-contained pattern. Kafka + Connect earn their complexity when:
- You have many consumers for the same data (Mongo change → search index + analytics + cache + audit + downstream service).
- You have multiple source DBs that should produce events in the same format (Postgres + Mongo + MySQL all into one Kafka topic family).
- You need replayable storage of events for days/weeks (Kafka retention) independent of source DB oplog.
- You're already on Kafka for other reasons.
If none apply, stick with Post 20. Connect adds: Kafka cluster, Schema Registry, Connect cluster, connector configs to maintain.
Chapter 2 — Architecture
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
┌──────────────┐ ┌──────────────┐
│ MongoDB │ │ MongoDB │
│ (source) │ │ (sink) │
└──────┬───────┘ └──────▲───────┘
│ change stream │
▼ │
┌──────────────────────┐ ┌──────────────────────┐
│ Mongo Source │ │ Mongo Sink │
│ Connector │ │ Connector │
└──────┬───────────────┘ └──────▲───────────────┘
│ │
▼ │
┌────────────────────────────┐
│ Kafka Cluster │
│ topics: orders, users… │
└────────────────────────────┘
▲
│
┌──────────────────────┐ │
│ Debezium Postgres │ │
│ Source Connector │────┘
└──────────────────────┘
▲
│ WAL
┌──────┴───────┐
│ Postgres │
└──────────────┘
Three connectors, two directions, one Kafka cluster.
Chapter 3 — MongoDB Kafka Source Connector
Reads from MongoDB change streams, publishes to Kafka topics.
Install: download the Confluent Hub connector or pull the JAR; place in Connect plugin path.
Config:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"name": "mongo-source-orders",
"config": {
"connector.class": "com.mongodb.kafka.connect.MongoSourceConnector",
"connection.uri": "mongodb+srv://user:pw@host/?retryWrites=true",
"database": "shop",
"collection": "orders",
"topic.prefix": "shop",
"publish.full.document.only": "true",
"change.stream.full.document": "updateLookup",
"copy.existing": "true",
"errors.tolerance": "all",
"errors.log.enable": "true"
}
}
Topic name produced: shop.shop.orders (<prefix>.<database>.<collection>).
Key options:
publish.full.document.only: true→ publish only the post-image document, not the full change event envelope. Often what consumers actually want.change.stream.full.document: updateLookup→ fetch the post-image on update events (Post 12 Ch 4).copy.existing: true→ on first start, snapshot the collection into Kafka before starting the change stream.pipeline→ an aggregation pipeline filter applied to events.
Apply via Connect REST API:
1
2
curl -X POST http://localhost:8083/connectors -H "Content-Type: application/json" -d @source.json
Chapter 4 — MongoDB Kafka Sink Connector
Reads from Kafka topics, writes to a Mongo collection.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"name": "mongo-sink-orders",
"config": {
"connector.class": "com.mongodb.kafka.connect.MongoSinkConnector",
"connection.uri": "mongodb+srv://user:pw@host/",
"database": "shop",
"collection": "orders_view",
"topics": "shop.shop.orders",
"document.id.strategy": "com.mongodb.kafka.connect.sink.processor.id.strategy.PartialValueStrategy",
"document.id.strategy.partial.value.projection.list": "_id",
"writemodel.strategy": "com.mongodb.kafka.connect.sink.writemodel.strategy.ReplaceOneBusinessKeyStrategy",
"errors.tolerance": "all",
"errors.deadletterqueue.topic.name": "shop.orders.dlq"
}
}
Key options:
document.id.strategy— how to derive_idon the sink doc. Use a strategy that keys on a stable business ID (often_idfrom the source).writemodel.strategy— how to apply each Kafka message.ReplaceOneBusinessKeyStrategyis upsert-by-key — idempotent, replay-safe.errors.tolerance: all+ DLQ topic — bad messages don't stop the connector.
This pattern (Source → Kafka → Sink) is how you build a cross-region Mongo replica via Kafka, populate a read-model collection, or sync to another store.
Chapter 5 — Debezium MongoDB Source Connector
Debezium offers its own MongoDB source connector — equivalent to MongoDB's official Source but with the Debezium event envelope (consistent across all Debezium connectors). Pick one or the other; both work.
Default envelope:
1
2
3
4
5
6
7
8
9
10
11
12
{
"schema": { ... },
"payload": {
"op": "c", // c=create, u=update, d=delete, r=read snapshot
"before": null,
"after": { "_id": "o1", ... },
"source": { "ts_ms": ..., "db": "shop", "collection": "orders", ... },
"ts_ms": ...,
"transaction": null
}
}
Useful when downstream consumers want before/after + a uniform shape across Postgres + Mongo + MySQL events.
Choose MongoDB's official Source connector if you only need one direction + want simpler messages. Choose Debezium if you're standardising on its envelope.
Chapter 6 — Debezium Postgres → MongoDB Sink
The killer use case: continuously replicate Postgres tables to MongoDB without writing code.
Debezium Postgres source
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"name": "pg-source",
"config": {
"connector.class": "io.debezium.connector.postgresql.PostgresConnector",
"database.hostname": "pg.example.com",
"database.dbname": "shop",
"database.user": "debezium",
"database.password": "...",
"topic.prefix": "pg.shop",
"plugin.name": "pgoutput",
"publication.autocreate.mode": "filtered",
"table.include.list": "public.orders,public.users",
"decimal.handling.mode": "string"
}
}
Publishes one topic per table — pg.shop.public.orders, etc.
MongoDB Sink for Debezium events
1
2
3
4
5
6
7
8
9
10
11
12
{
"name": "debezium-mongo-sink",
"config": {
"connector.class": "io.debezium.connector.mongodb.MongoDbSinkConnector",
"topics": "pg.shop.public.orders",
"mongodb.connection.string": "mongodb+srv://user:pw@host/",
"sink.database": "shop_synced",
"sink.collection": "orders",
"delete.tombstone.handling.mode": "delete"
}
}
Debezium ships a dedicated MongoDB sink connector that understands its own event envelope + handles op: 'd' deletes correctly. Recent versions support the standard Debezium connector pattern (every event type → matching mongo op).
Watch out: the older MongoDB Kafka Sink (Chapter 4) doesn't natively handle Debezium update events from other DBs — you need a CDC handler (
ChangeStreamHandler) configured, and there have been update-event bugs (see references). For new Postgres→Mongo work in 2026, prefer the Debezium MongoDB sink — it's purpose-built.
Chapter 7 — Kafka topics + partitioning
For Mongo source events:
- One topic per collection (default) — easy.
- Topic key = document
_id→ events for the same doc land in the same partition → preserved order per doc.
For Debezium Postgres source:
- One topic per table.
- Key = primary key → ordered per row.
Partition count = expected throughput. Default 6-12 partitions per topic for medium throughput. Don't go to 100 unless you have parallel consumers to match.
Retention: 7 days default. For replay scenarios, 30 days. For Event Sourcing (Post 21), forever (compact topic or off-cluster archive).
Chapter 8 — Schema Registry + Avro / JSON Schema
Without a registry, every consumer must parse JSON heuristically + handle schema changes manually.
With Confluent Schema Registry (or Apicurio, Karapace):
- Producers register schemas.
- Consumers fetch schema by ID embedded in each message.
- Compatibility rules enforced (backward, forward, full).
Format choice: Avro (binary, compact, strong typing) for high-volume; JSON Schema (human-readable, debuggable) for moderate. Avro is the Kafka native; JSON Schema gaining ground.
Connectors support both. Configure once; downstream consumers get type safety + evolution rules for free.
Chapter 9 — Deduplication + ordering — same rules as outbox
Kafka delivers at-least-once (default). Consumers must be idempotent.
Patterns:
- Key by business ID → idempotent upserts at the sink.
- Optimistic versioning — sink rejects events with
version <= current. - Tombstone records for deletes (key = ID, value = null) — compaction-friendly.
Ordering:
- Within a partition: guaranteed.
- Across partitions: not guaranteed.
- Key your events so all events for one entity land in one partition.
Chapter 10 — Local dev environment
docker-compose.yml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
services:
zookeeper:
image: confluentinc/cp-zookeeper:7.6.0
environment: { ZOOKEEPER_CLIENT_PORT: 2181 }
kafka:
image: confluentinc/cp-kafka:7.6.0
depends_on: [zookeeper]
ports: ["9092:9092"]
environment:
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
connect:
image: confluentinc/cp-kafka-connect:7.6.0
depends_on: [kafka]
ports: ["8083:8083"]
environment:
CONNECT_BOOTSTRAP_SERVERS: kafka:9092
CONNECT_GROUP_ID: dev
CONNECT_CONFIG_STORAGE_TOPIC: connect.config
CONNECT_OFFSET_STORAGE_TOPIC: connect.offset
CONNECT_STATUS_STORAGE_TOPIC: connect.status
CONNECT_KEY_CONVERTER: org.apache.kafka.connect.json.JsonConverter
CONNECT_VALUE_CONVERTER: org.apache.kafka.connect.json.JsonConverter
CONNECT_PLUGIN_PATH: /usr/share/java,/usr/share/confluent-hub-components
volumes:
- ./plugins:/usr/share/confluent-hub-components
mongo:
image: mongo:8
command: ["--replSet", "rs0"]
ports: ["27017:27017"]
postgres:
image: postgres:16
environment: { POSTGRES_USER: ..., POSTGRES_PASSWORD: ..., POSTGRES_DB: shop }
ports: ["5432:5432"]
command: ["postgres", "-c", "wal_level=logical"]
./plugins holds the downloaded connector JARs (Mongo source/sink + Debezium Postgres). confluent-hub install is the friendly path.
After docker compose up, hit http://localhost:8083 for Connect REST. POST connector configs as in Chapters 3-6.
Chapter 11 — Production operations
- Connect cluster in distributed mode (multiple Connect workers) for HA + scalability.
- Monitoring: JMX → Prometheus exporter → Grafana. Watch connector lag + DLQ size.
- Backups: Connect stores state in Kafka topics — back them up.
- Schema changes: register new schema versions; downstream consumers handle gracefully.
- Failover: source connector resumes from stored offset on restart.
- Atlas: hosted Confluent Cloud + Atlas Source/Sink work seamlessly. Many teams pick this combo for zero ops.
Chapter 12 — Comparison table
| Outbox + change-stream (Post 20) | Mongo Source/Sink | Debezium + Sink | |
|---|---|---|---|
| Setup complexity | minimal | medium (Kafka + Connect) | medium-high (Kafka + Connect + Debezium) |
| Cross-DB CDC | no | no | yes (Postgres / MySQL / etc) |
| Replay window | as long as you keep outbox rows |
Kafka retention (7-90 days) | Kafka retention |
| Event envelope | yours | Mongo's | Debezium's (consistent across DBs) |
| Multi-consumer fan-out | possible but custom | natural via Kafka | natural via Kafka |
| Cost | tiny | Kafka + Connect | Kafka + Connect |
Pick the cheapest pattern that fits the requirement.
Chapter 13 — Common pitfalls
| Pitfall | Cause | Fix |
|---|---|---|
| Mongo Sink fails on update from Debezium | CDC handler config missing | use Debezium MongoDB sink instead, or configure ChangeStreamHandler |
Update events arrive without fullDocument |
source change.stream.full.document not set |
set to updateLookup |
| Out-of-order events | partition key not on business ID | re-key |
| Connector lag grows | sink slow | scale sink concurrency; partition more |
| Schema change breaks consumers | no Schema Registry | adopt; enforce compatibility |
| DLQ topic ignored | no alerts on DLQ size | alert when DLQ count > 0 |
| Connect single-node failure | not running in distributed mode | run ≥ 2 workers |
| Source replicates everything (huge load) | no pipeline filter |
filter event stream at source |
| Tombstone confusion | deletes received as null | configure handler |
| Debezium PG sequence reset / re-init | snapshot mode misconfigured | review snapshot.mode |
Practice Exercises
Exercise 1 — Local stack
Stand up the docker-compose.yml from Chapter 10. Confirm Kafka, Connect, Mongo (replica set), Postgres all run.
Exercise 2 — Mongo Source
Configure the Mongo Source connector for a shop.orders collection. Insert into Mongo; consume the shop.shop.orders topic with kafka-console-consumer.
Exercise 3 — Mongo Sink
Add a Mongo Sink consuming the topic into shop_view.orders. Insert into source; verify it appears in the sink within seconds.
Exercise 4 — Debezium Postgres → Mongo
Configure Debezium Postgres source + MongoDB sink. Insert into a Postgres table; verify the document appears in Mongo.
Exercise 5 — Filter at source
Add a pipeline filter to the Mongo Source connector — only events with status: "paid". Confirm filter works.
Exercise 6 — DLQ
Misconfigure a sink (wrong field type). Send a bad message; confirm it lands in the DLQ topic instead of stopping the connector.
Cheat-sheet
| What | How |
|---|---|
| Mongo → Kafka | MongoDB Kafka Source connector (official) |
| Kafka → Mongo | MongoDB Kafka Sink connector |
| Postgres → Mongo | Debezium PG source + Debezium MongoDB sink |
| Mongo → other DB | MongoDB Source + that DB's sink |
| Full doc on update | change.stream.full.document: updateLookup |
| Publish post-image only | publish.full.document.only: true |
| Snapshot on first run | copy.existing: true |
| Idempotent sink | ReplaceOneBusinessKeyStrategy on _id |
| Error policy | errors.tolerance: all + DLQ topic |
| Schema | use Schema Registry (Avro / JSON Schema) |
| Order per entity | key on business ID → same partition |
| Local dev | docker-compose with Kafka + Connect + Mongo + PG |
| Production | distributed Connect cluster + monitoring + alerts |
| Cheaper alt for single consumer | outbox + change-stream publisher (Post 20) |
What's next
Post 25 covers testing strategies — Testcontainers + in-memory MongoDB + fixtures + Jest/Vitest/pytest integration patterns.
References
- MongoDB Kafka Connector docs
- Debezium Postgres connector
- Debezium MongoDB sink
- Debezium MongoDB source
- MongoDB Sink CDC Handlers
- Confluent — MongoDB Atlas + Confluent Cloud
Note: this post is in progress. Planned additions: rolling schema migration with Schema Registry; topic compaction tradeoffs; Kafka Streams for in-flight enrichment.










