agentsclimarketplace

Cdc debezium

Skill ivanshamaev/de-agent-skills/skills/cdc_debezium

Change Data Capture with Debezium — PostgreSQL/MySQL/Oracle CDC connectors, change event structure (before/after/op/source), snapshot modes, Kafka Connect deployment, SMT transformations, outbox pattern, Iceberg/Delta sink integration, idempotency guaranteesFrom its SKILL.md

Install
npx -y skills add ivanshamaev/de-agent-skills --skill cdc_debezium

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 15 stars15 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

22.1 KB, ~5.5k tokens by cl100k_base, as published. Nobody here has run it

CDC Pipelines with Debezium

When to Use

Activate this skill when the task involves:

  • Setting up Debezium connectors for PostgreSQL, MySQL, or Oracle
  • Interpreting or transforming CDC change events (insert/update/delete/truncate)
  • Designing CDC pipelines: database → Kafka → data lake / warehouse
  • Implementing the Outbox pattern for transactional CDC
  • Troubleshooting replication lag, snapshot failures, or schema evolution
  • Integrating CDC streams with Apache Flink, Spark Structured Streaming, or dbt

Core Architecture

┌──────────────────────────────────────────────────────────────┐
│  Source Database                                             │
│  ┌─────────────┐  WAL / binlog / redo log                   │
│  │ PostgreSQL  │──────────────────────┐                     │
│  │ MySQL       │                      ▼                     │
│  │ Oracle      │          ┌───────────────────────┐         │
│  └─────────────┘          │  Kafka Connect Worker │         │
│                           │  ┌─────────────────┐  │         │
│                           │  │ Debezium Source │  │         │
│                           │  │    Connector    │  │         │
│                           │  └────────┬────────┘  │         │
│                           └───────────│───────────┘         │
│                                       ▼                     │
│                           ┌───────────────────────┐         │
│                           │  Kafka Topic          │         │
│                           │  <prefix>.<db>.<tbl>  │         │
│                           └───────────┬───────────┘         │
│                                       ▼                     │
│                    ┌──────────────────────────────┐         │
│                    │  Consumers                   │         │
│                    │  • Kafka Connect Sink         │         │
│                    │  • Apache Flink / Spark       │         │
│                    │  • ksqlDB / Kafka Streams     │         │
│                    └──────────────────────────────┘         │
└──────────────────────────────────────────────────────────────┘

Debezium reads database transaction logs (WAL for PostgreSQL, binlog for MySQL), not the tables themselves — zero query load on the source.


Change Event Structure

Every Debezium event is a Kafka message with a structured envelope:

{
  "schema": { ... },
  "payload": {
    "before": {
      "id": 1001,
      "name": "Alice",
      "email": "[email protected]",
      "updated_at": 1700000000000
    },
    "after": {
      "id": 1001,
      "name": "Alice Smith",
      "email": "[email protected]",
      "updated_at": 1700001000000
    },
    "source": {
      "version": "2.5.0.Final",
      "connector": "postgresql",
      "name": "pgserver1",
      "ts_ms": 1700001000123,
      "snapshot": "false",
      "db": "mydb",
      "sequence": "[\"24023119\",\"24023255\"]",
      "schema": "public",
      "table": "customers",
      "txId": 756,
      "lsn": 24023255,
      "xmin": null
    },
    "op": "u",
    "ts_ms": 1700001000456,
    "transaction": {
      "id": "756:24023255",
      "total_order": 1,
      "data_collection_order": 1
    }
  }
}

Operation Types

opMeaningbeforeafter
cINSERT (create)nullrow state
uUPDATEprevious statenew state
dDELETEprevious statenull
rREAD (snapshot)nullrow state
tTRUNCATEnullnull

Tombstone Events

After a d (delete) event, Debezium emits a tombstone — a message with null value and the same key — enabling Kafka log compaction to eventually remove the record.


PostgreSQL Connector

Prerequisites

-- postgresql.conf: must be set before Debezium can connect
ALTER SYSTEM SET wal_level = 'logical';
ALTER SYSTEM SET max_replication_slots = 10;
ALTER SYSTEM SET max_wal_senders = 10;

-- Create dedicated Debezium user
CREATE ROLE debezium WITH LOGIN PASSWORD 'secret' REPLICATION;
GRANT CONNECT ON DATABASE mydb TO debezium;
GRANT USAGE ON SCHEMA public TO debezium;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO debezium;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO debezium;

-- Create publication (pgoutput plugin — no extra extension needed)
CREATE PUBLICATION dbz_pub FOR TABLE customers, orders, products;
-- Or capture all tables:
CREATE PUBLICATION dbz_pub FOR ALL TABLES;

Restart PostgreSQL after changing wal_level.

Connector Configuration

{
  "name": "pg-customers-connector",
  "config": {
    "connector.class": "io.debezium.connector.postgresql.PostgresConnector",
    "tasks.max": "1",

    "database.hostname": "postgres-host",
    "database.port": "5432",
    "database.user": "debezium",
    "database.password": "secret",
    "database.dbname": "mydb",

    "topic.prefix": "pgserver1",
    "plugin.name": "pgoutput",
    "publication.name": "dbz_pub",
    "slot.name": "debezium_slot",

    "table.include.list": "public.customers,public.orders",

    "snapshot.mode": "initial",
    "snapshot.locking.mode": "minimal",

    "heartbeat.interval.ms": "30000",
    "heartbeat.topics.prefix": "__debezium-heartbeat",

    "key.converter": "io.confluent.kafka.serializers.KafkaAvroSerializer",
    "key.converter.schema.registry.url": "http://schema-registry:8081",
    "value.converter": "io.confluent.kafka.serializers.KafkaAvroSerializer",
    "value.converter.schema.registry.url": "http://schema-registry:8081",

    "transforms": "unwrap",
    "transforms.unwrap.type": "io.debezium.transforms.ExtractNewRecordState",
    "transforms.unwrap.drop.tombstones": "false",
    "transforms.unwrap.delete.handling.mode": "rewrite",
    "transforms.unwrap.add.fields": "op,table,lsn,source.ts_ms",
    "transforms.unwrap.add.headers": "db",

    "errors.tolerance": "all",
    "errors.deadletterqueue.topic.name": "dlq.pgserver1.customers",
    "errors.deadletterqueue.topic.replication.factor": "3",
    "errors.deadletterqueue.context.headers.enable": "true"
  }
}

Snapshot Modes (PostgreSQL)

ModeDescriptionWhen to Use
initialSnapshot on first start only; then streamDefault — new deployments
initial_onlySnapshot then stop — no streamingOne-time historical load
no_dataSkip snapshot, start streaming from nowAppend-only use cases
alwaysSnapshot on every connector restartDev/testing only
exportedConsistent snapshot without table locks (PG 15+)Large tables in production
incrementalRe-snapshot selected tables without stoppingAdd new tables mid-stream

Replication Slot Monitoring

-- Check lag: drop-behind LSN distance
SELECT
    slot_name,
    pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS lag_size,
    active,
    database
FROM pg_replication_slots
WHERE slot_type = 'logical';

-- Alert if lag_size > 5GB — Debezium consumer is too slow

MySQL Connector

Prerequisites

# my.cnf — server-id must be unique across all replicas
server-id         = 12345
log_bin           = mysql-bin
binlog_format     = ROW
binlog_row_image  = FULL
expire_logs_days  = 7
gtid_mode         = ON    # recommended
enforce_gtid_consistency = ON
CREATE USER 'debezium'@'%' IDENTIFIED BY 'secret';
GRANT SELECT, RELOAD, SHOW DATABASES, REPLICATION SLAVE, REPLICATION CLIENT
    ON *.* TO 'debezium'@'%';
FLUSH PRIVILEGES;

Connector Configuration

{
  "name": "mysql-orders-connector",
  "config": {
    "connector.class": "io.debezium.connector.mysql.MySqlConnector",
    "tasks.max": "1",

    "database.hostname": "mysql-host",
    "database.port": "3306",
    "database.user": "debezium",
    "database.password": "secret",
    "database.server.id": "12345",

    "topic.prefix": "mysqlserver1",
    "database.include.list": "shop",
    "table.include.list": "shop.orders,shop.order_items",

    "snapshot.mode": "initial",
    "snapshot.locking.mode": "minimal",

    "include.schema.changes": "true",
    "schema.history.internal.kafka.topic": "schema-changes.shop",
    "schema.history.internal.kafka.bootstrap.servers": "kafka:9092",

    "key.converter": "io.confluent.kafka.serializers.KafkaAvroSerializer",
    "key.converter.schema.registry.url": "http://schema-registry:8081",
    "value.converter": "io.confluent.kafka.serializers.KafkaAvroSerializer",
    "value.converter.schema.registry.url": "http://schema-registry:8081",

    "transforms": "unwrap",
    "transforms.unwrap.type": "io.debezium.transforms.ExtractNewRecordState",
    "transforms.unwrap.drop.tombstones": "false",
    "transforms.unwrap.delete.handling.mode": "rewrite",
    "transforms.unwrap.add.fields": "op,table,source.ts_ms"
  }
}

MySQL requires schema.history.internal.kafka.topic — a durable log of all DDL changes. Never delete this topic.


Single Message Transforms (SMTs)

ExtractNewRecordState — Flattening Events

Without SMT, downstream consumers receive the full before/after/source envelope. ExtractNewRecordState flattens it:

Before SMT:

{"payload": {"before": null, "after": {"id": 1, "name": "Alice"}, "op": "c"}}

After SMT:

{"id": 1, "name": "Alice", "__op": "c", "__table": "customers", "__source_ts_ms": 1700001000123}

Field Masking for PII

"transforms": "mask_email",
"transforms.mask_email.type": "org.apache.kafka.connect.transforms.MaskField$Value",
"transforms.mask_email.fields": "email,phone",
"transforms.mask_email.replacement": "***"

Routing to Per-Table Topics

"transforms": "route",
"transforms.route.type": "io.debezium.transforms.ByLogicalTableRouter",
"transforms.route.topic.regex": "pgserver1\\.public\\.(.*)",
"transforms.route.topic.replacement": "cdc.raw.$1"

Kafka Connect Deployment

Docker Compose (Standalone)

version: "3.8"
services:
  kafka:
    image: confluentinc/cp-kafka:7.6.0
    environment:
      KAFKA_NODE_ID: 1
      KAFKA_PROCESS_ROLES: broker,controller
      KAFKA_CONTROLLER_QUORUM_VOTERS: "1@kafka:9093"
      KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
      CLUSTER_ID: "MkU3OEVBNTcwNTJENDM2Qk"

  schema-registry:
    image: confluentinc/cp-schema-registry:7.6.0
    environment:
      SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: kafka:9092
      SCHEMA_REGISTRY_HOST_NAME: schema-registry
    ports:
      - "8081:8081"

  kafka-connect:
    image: debezium/connect:2.5
    environment:
      BOOTSTRAP_SERVERS: kafka:9092
      GROUP_ID: debezium-connect-cluster
      CONFIG_STORAGE_TOPIC: connect-configs
      OFFSET_STORAGE_TOPIC: connect-offsets
      STATUS_STORAGE_TOPIC: connect-statuses
      CONFIG_STORAGE_REPLICATION_FACTOR: 1
      OFFSET_STORAGE_REPLICATION_FACTOR: 1
      STATUS_STORAGE_REPLICATION_FACTOR: 1
      CONNECT_KEY_CONVERTER: io.confluent.kafka.serializers.KafkaAvroSerializer
      CONNECT_VALUE_CONVERTER: io.confluent.kafka.serializers.KafkaAvroSerializer
      CONNECT_KEY_CONVERTER_SCHEMA_REGISTRY_URL: http://schema-registry:8081
      CONNECT_VALUE_CONVERTER_SCHEMA_REGISTRY_URL: http://schema-registry:8081
    ports:
      - "8083:8083"
    depends_on:
      - kafka
      - schema-registry

Connector Lifecycle via REST API

# Deploy connector
curl -s -X POST http://localhost:8083/connectors \
  -H "Content-Type: application/json" \
  -d @pg-customers-connector.json

# Check status
curl -s http://localhost:8083/connectors/pg-customers-connector/status | jq

# Pause / Resume (drains without losing offset)
curl -X PUT http://localhost:8083/connectors/pg-customers-connector/pause
curl -X PUT http://localhost:8083/connectors/pg-customers-connector/resume

# Restart failed task
curl -X POST "http://localhost:8083/connectors/pg-customers-connector/tasks/0/restart"

# List all connectors
curl -s http://localhost:8083/connectors | jq

# Delete connector (releases replication slot!)
curl -X DELETE http://localhost:8083/connectors/pg-customers-connector

Outbox Pattern

The Outbox pattern guarantees exactly-once delivery of domain events without dual writes or distributed transactions.

PostgreSQL Setup

-- Outbox table lives in the same database as the domain table
CREATE TABLE outbox (
    id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    aggregate_type VARCHAR(255) NOT NULL,   -- e.g. 'Order'
    aggregate_id   VARCHAR(255) NOT NULL,   -- e.g. order_id
    event_type     VARCHAR(255) NOT NULL,   -- e.g. 'OrderPlaced'
    payload        JSONB         NOT NULL,
    created_at     TIMESTAMPTZ   NOT NULL DEFAULT NOW()
);

-- Application writes domain change + outbox row in ONE transaction
BEGIN;
  INSERT INTO orders (id, customer_id, total) VALUES (42, 7, 199.99);
  INSERT INTO outbox (aggregate_type, aggregate_id, event_type, payload)
  VALUES ('Order', '42', 'OrderPlaced',
    '{"order_id": 42, "customer_id": 7, "total": 199.99}');
COMMIT;

Outbox Connector Configuration

{
  "name": "outbox-connector",
  "config": {
    "connector.class": "io.debezium.connector.postgresql.PostgresConnector",
    "topic.prefix": "outbox",
    "table.include.list": "public.outbox",
    "plugin.name": "pgoutput",
    "publication.name": "dbz_outbox_pub",
    "slot.name": "debezium_outbox_slot",

    "transforms": "outbox",
    "transforms.outbox.type": "io.debezium.transforms.outbox.EventRouter",
    "transforms.outbox.table.field.event.id": "id",
    "transforms.outbox.table.field.event.key": "aggregate_id",
    "transforms.outbox.table.field.event.type": "event_type",
    "transforms.outbox.table.field.event.payload": "payload",
    "transforms.outbox.route.by.field": "aggregate_type",
    "transforms.outbox.route.topic.replacement": "events.${routedByValue}.Avro",
    "transforms.outbox.table.expand.json.payload": "true",

    "tombstones.on.delete": "false"
  }
}

The EventRouter SMT routes each row to events.Order.Avro, events.Payment.Avro, etc. and tombstones deleted outbox rows (after cleanup jobs purge them).


Consuming CDC Events — Python

from confluent_kafka import Consumer, KafkaException
from confluent_kafka.schema_registry.avro import AvroDeserializer
from confluent_kafka.serialization import SerializationContext, MessageField
import json

consumer = Consumer({
    "bootstrap.servers": "kafka:9092",
    "group.id": "cdc-consumer-group",
    "auto.offset.reset": "earliest",
    "enable.auto.commit": False,
    "max.poll.interval.ms": 300000,
})
consumer.subscribe(["pgserver1.public.customers"])

try:
    while True:
        msg = consumer.poll(timeout=1.0)
        if msg is None:
            continue
        if msg.error():
            raise KafkaException(msg.error())

        # With ExtractNewRecordState SMT, value is a flat dict
        value = json.loads(msg.value())
        op = value.get("__op", "r")

        if op == "c":
            upsert_to_sink(value)
        elif op == "u":
            upsert_to_sink(value)
        elif op == "d":
            delete_from_sink(value["id"])

        consumer.commit(asynchronous=False)
finally:
    consumer.close()

Apache Flink CDC Integration

Flink SQL can consume Debezium-formatted Kafka topics natively via the debezium-json format.

-- Flink SQL: source table reading Debezium JSON from Kafka
CREATE TABLE customers_cdc (
    id          BIGINT,
    name        STRING,
    email       STRING,
    updated_at  TIMESTAMP(3),
    PRIMARY KEY (id) NOT ENFORCED
) WITH (
    'connector'  = 'kafka',
    'topic'      = 'pgserver1.public.customers',
    'properties.bootstrap.servers' = 'kafka:9092',
    'properties.group.id'          = 'flink-cdc-group',
    'scan.startup.mode'            = 'earliest-offset',
    'format'                       = 'debezium-json',
    'debezium-json.schema-include' = 'false'
);

-- Flink SQL: Iceberg sink (upsert semantics)
CREATE TABLE customers_iceberg (
    id        BIGINT,
    name      STRING,
    email     STRING,
    PRIMARY KEY (id) NOT ENFORCED
) WITH (
    'connector'          = 'iceberg',
    'catalog-name'       = 'rest',
    'catalog-type'       = 'rest',
    'uri'                = 'http://iceberg-rest:8181',
    'warehouse'          = 's3://lakehouse/warehouse',
    'database-name'      = 'silver',
    'table-name'         = 'customers'
);

INSERT INTO customers_iceberg
SELECT id, name, email FROM customers_cdc;

For Avro with Schema Registry, use 'format' = 'debezium-avro-confluent' and set 'debezium-avro-confluent.url'.


Iceberg / Delta Sink via Kafka Connect

Iceberg Sink Connector (Tabular)

{
  "name": "iceberg-sink-customers",
  "config": {
    "connector.class": "io.tabular.iceberg.connect.IcebergSinkConnector",
    "tasks.max": "2",
    "topics": "pgserver1.public.customers",

    "iceberg.catalog.type": "rest",
    "iceberg.catalog.uri": "http://iceberg-rest:8181",
    "iceberg.catalog.warehouse": "s3://lakehouse/warehouse",

    "iceberg.tables": "silver.customers",
    "iceberg.tables.upsert-mode-enabled": "true",
    "iceberg.tables.cdc-field": "__op",

    "iceberg.control.topic": "iceberg-control",
    "iceberg.control.group-id": "iceberg-sink-group",
    "iceberg.commit.interval-ms": "60000",
    "iceberg.commit.timeout-ms": "30000"
  }
}

Schema Evolution Handling

Debezium propagates DDL changes automatically. Key behaviors:

ChangePostgreSQL (pgoutput)MySQL
ADD COLUMNNew field appears in afterSchema history updated; new events include column
DROP COLUMNField disappears from afterSchema history updated
RENAME COLUMNTreat as drop + add (no rename event)Same
ALTER COLUMN TYPESupported for compatible typesSupported
TRUNCATE TABLEop: "t" event emittedEmits op: "t"

For downstream Iceberg/Delta sinks, enable schema evolution on the sink connector to auto-add new columns rather than failing.


Monitoring

Key JMX Metrics

MetricConnector TypeAlert Threshold
MilliSecondsBehindSourceAll> 60,000 ms
NumberOfCommittedTransactionsMySQLRate drop to 0
SnapshotRunningAllStuck > expected time
TotalNumberOfErrorsSeenAll> 0
QueueRemainingCapacityAll< 10%

PostgreSQL Replication Lag

SELECT
    application_name,
    pg_size_pretty(pg_wal_lsn_diff(
        sent_lsn, replay_lsn
    )) AS replay_lag,
    state
FROM pg_stat_replication;

Heartbeat Configuration

Without heartbeats, a quiet source database (no writes) will not advance the replication slot LSN, causing WAL accumulation.

"heartbeat.interval.ms": "30000",
"heartbeat.action.query": "INSERT INTO debezium_heartbeat VALUES (DEFAULT) ON CONFLICT DO NOTHING"

Create the heartbeat table:

CREATE TABLE IF NOT EXISTS debezium_heartbeat (
    id SERIAL PRIMARY KEY,
    ts TIMESTAMPTZ DEFAULT now()
);

Anti-Patterns

  1. Not monitoring replication slot lag — WAL accumulates unboundedly; PostgreSQL disk fills up. Set alert at 5 GB lag.

  2. Deleting the replication slot externally — kills the connector offset; forces full re-snapshot. Never drop slots manually.

  3. Using snapshot.mode: always in production — re-snapshots entire tables on every restart, causing massive traffic and duplicates downstream.

  4. One connector for all tables — a single failed task blocks all tables. Split high-throughput or high-risk tables into separate connectors.

  5. Missing heartbeat on write-quiet databases — WAL accumulates when source tables have no writes. Always configure heartbeat.interval.ms.

  6. Not enabling the DLQ — deserialization or routing errors silently stop the connector task. Always configure errors.tolerance: all + errors.deadletterqueue.topic.name.

  7. Dual writes (app writes DB + Kafka) — violates atomicity. Use the Outbox pattern instead.

  8. Using schema_only snapshot mode (deprecated) — replaced by no_data. Using it produces a warning and may not be supported in future versions.

  9. No tombstones.on.delete handling in sink — sinks that don't process tombstones accumulate ghost records in compacted topics.

  10. Transforming events inside Kafka Connect for complex logic — SMTs are for simple field mapping only. Route raw events to Kafka and apply transformation in Flink or Spark.


References to Consult When Needed

  • Debezium PostgreSQL connector reference: debezium.io/documentation/reference/stable/connectors/postgresql.html
  • Debezium MySQL connector reference: debezium.io/documentation/reference/stable/connectors/mysql.html
  • ExtractNewRecordState SMT: debezium.io/documentation/reference/stable/transformations/event-flattening.html
  • Outbox EventRouter SMT: debezium.io/documentation/reference/stable/transformations/outbox-event-router.html
  • Flink Debezium format: nightlies.apache.org/flink/flink-docs-stable/docs/connectors/table/formats/debezium/

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.