Introduction
Running Debezium in production is very different from running it locally. In production, the pipeline must be:
- Reliable: no lost or duplicated events.
- Ordered: events for the same entity must arrive in sequence.
- Observable: operators must know if the system is alive.
- Scalable: handle high throughput with multiple consumers.
This article shows how to build a production-grade Debezium + Kafka connector using the Outbox Pattern with PostgreSQL in an e-commerce order system.
The Outbox Pattern in E-Commerce
In an online shop:
- A new order (
ORD-20250927-123
) is created in theorders
table. - At the same time, the app inserts a record into the
order_outbox
table.
This guarantees:
- If the order exists, the event also exists.
- No “dual write” problem between DB and Kafka.
- Debezium can safely capture outbox records and publish them to Kafka.
Production-Grade Connector Config
{ "name": "ecommerce-order-outbox-connector", "config": { "connector.class": "io.debezium.connector.postgresql.PostgresConnector", "database.hostname": "ecom-prod-db.example.com", "database.port": "5432", "database.user": "cdc_user", "database.password": "******", "database.dbname": "ecommerce_production", "database.server.name": "ecommerce", "publication.name": "prd_order_outbox_cdc", "slot.name": "cdc_order_outbox", "plugin.name": "pgoutput", "table.include.list": "public.order_outbox(.*)", // assume table is partitioned "message.key.columns": "public.order_outbox(.*):order_id", "transforms": "Reroute, unwrap", "transforms.Reroute.type": "io.debezium.transforms.ByLogicalTableRouter", "transforms.Reroute.topic.regex": "(.*)order_outbox(.*)", "transforms.Reroute.topic.replacement": "ecommerce.public.order_outbox", "transforms.Reroute.key.enforce.uniqueness": false, "transforms.unwrap.type": "io.debezium.transforms.ExtractNewRecordState", "key.converter": "org.apache.kafka.connect.json.JsonConverter", "key.converter.schemas.enable": false, "value.converter": "org.apache.kafka.connect.json.JsonConverter", "value.converter.schemas.enable": false, "tasks.max": "3", "topic.creation.enable": "true", "topic.creation.default.partitions": "12", "topic.creation.default.replication.factor": "3", "topic.creation.default.retention.ms": "604800000", "exactly.once.support": "required", "exactly.once.source.support": "enabled", "errors.log.enable": "true", "errors.log.include.messages": "true", "errors.deadletterqueue.topic.name": "dead-letter-queue", "errors.deadletterqueue.context.headers.enable": "true", "heartbeat.interval.ms": "10000", "heartbeat.topics.prefix": "heartbeat_order_outbox", "heartbeat.action.query": "INSERT INTO public.\"_heartbeat_apps\" (name, heartbeat)\nVALUES ('order_outbox', NOW())\nON CONFLICT (name)\nDO UPDATE SET heartbeat = NOW();" } }
Key Features
Partitioning by Order ID
Ensures that all events for the same order_id
go to the same partition: correct order for shipping, billing, and fraud detection.
Why Ordering Key Matters
- The ordering key (partition key) decides event order in Kafka.
- Wrong key → lost ordering → events like Shipped may arrive before Created.
- This causes:
- Out-of-order processing.
- Broken business logic.
- Race conditions.
- Expensive recovery.
- Solution: choose the key that matches the business entity needing strict sequence (e.g.,
order_id
for order lifecycle,customer_id
for customer-level workflows).
Topic Routing with Logical Table Router
Normalizes all order_outbox
changes into one Kafka topic:
ecommerce.public.order_outbox
Clean Event Payload
ExtractNewRecordState
transform unwraps the Debezium envelope: produces simple JSON.
Exactly-Once Semantics
Avoids duplicates and ensures at-least-once delivery is upgraded to exactly-once when combined with Kafka transactional producers.
Publisher (Debezium / Producer):
- Uses idempotence → avoids duplicate sends.
- Uses transactions → all events in batch are written atomically.
- Commits WAL position + offsets inside the same transaction.
Consumer (Service):
- Reads partition in order (one consumer per partition).
- Commits offsets after successful processing.
- Must be idempotent (e.g., UPSERT, unique keys, track processed events).
Dead Letter Queue
Failed records are pushed into dead-letter-queue
for later inspection. Pipeline never blocks.
flowchart LR A[Debezium Connector] -->|Valid event| K[Kafka Topic] A -->|Error: bad record, schema, serialization| DLQ[Dead Letter Queue Topic] K --> C1[Consumer Service] DLQ --> OP[Ops Team / Monitoring]
Flow Explanation
- Debezium reads a DB change.
- If the event is valid: it is published to the normal Kafka topic.
- If the event fails (schema error, invalid payload, serialization issue): it is redirected to the DLQ topic.
- Consumers continue reading from the normal topic without interruption.
- Ops/Monitoring team inspects the DLQ, fixes root cause, and optionally reprocesses the events.
Heartbeats for Monitoring
Debezium supports two types of heartbeat:
Database Heartbeat (configured in connector):
- Inserts or updates a row in the DB (
_heartbeat_apps
table).Confirms Debezium is actively reading and writing.Useful for DBAs to check connector health from the database side.
name | heartbeat |
order_outbox | 2025-09-27 13:45:00.123 |
Kafka Heartbeat Topic (heartbeat_order_outbox
):
- Debezium publishes a heartbeat event into Kafka.Consumers can subscribe to this topic to check pipeline liveness.
Example Kafka heartbeat event: { "status": "alive", "ts_ms": 1758967500123 }
Consumer Metrics (additional best practice):
- Consumer services (e.g., billing or shipping service) emit their own metrics:
- Lag between event timestamp and processing time.
- Last processed
order_id
.
- Sent to Prometheus/Grafana for monitoring end-to-end latency.