Compare database-log change data capture with application/domain events by transaction coupling, semantics, ordering, schema, snapshots, offsets, and operational ownership.

Change Data Capture from Logs vs Application-Published Events

AtlasMart now turns committed data changes into recoverable synchronization contracts without confusing physical row changes with business meaning.

Advanced115–150 minutesCDC semantics labPython 3.13+ · standard library / sqlite3 where notedVendor-neutral · free/local mandatory pathLast reviewed: August 2026
01

Differentiate database-log CDC from application/domain events by semantics, coupling, schema, and ownership.

02

Explain offsets/log sequence numbers, snapshots, before/after images, transaction boundaries, and restart checkpoints.

03

Show why a CDC row change is not automatically a business event and why replay/duplicates must be expected.

04

Choose log-derived changes or explicitly published events based on the consumer contract rather than tooling fashion.

1. One order change, two very different contracts

AtlasMart updates an order from CREATED to PAID. A database can expose that fact from its transaction log through change data capture (CDC): machinery that converts committed storage/database changes into a consumable change stream. The application can also publish an application event or domain event such as OrderPaid. Both can describe the same underlying moment, but they have different meanings and owners.

A CDC record usually answers questions such as “which table/collection changed, which key changed, what was the before/after image, what transaction/log position produced it?” A domain event answers a business question: “what happened that downstream services are allowed to depend on?” Treating those contracts as interchangeable is a common long-lived integration mistake.

2. Log positions, snapshots, and transaction coupling

A database log is ordered according to the database's commit/log semantics. Consumers need a position—often called an offset, log sequence number, or connector-specific source position—to say how far they have processed. A CDC pipeline commonly combines an initial snapshot of existing rows with a change stream starting at a position that makes the snapshot and subsequent changes consistent.

Dimension Log CDC Application/domain event
Atomic coupling Derived from committed database changes Must be coordinated with the business transaction, commonly via outbox
Payload Storage/schema-oriented before/after change Intentional consumer contract
Ordering evidence Log position/transaction order within source scope Depends on publisher/broker partitioning and event key
Ownership Often data platform/database integration Application/domain team owns semantic meaning
Schema change risk Physical schema evolution can leak directly to consumers Semantic schema can evolve independently when designed well

3. Observable history: before/after images are evidence, not intent

The lab records WAL-like changes at positions 100, 110, and 120. The order table update at 110 has a before image CREATED and after image PAID. Application logic can map that transition to OrderPaid, but the mapping itself is domain knowledge. A generic CDC connector cannot know whether the same column transition represents capture, reconciliation, manual repair, import, or test traffic unless the source contract supplies that meaning.

Evidence boundary

A log position proves where a source change appeared in one source stream. It does not by itself provide a globally ordered business history across unrelated databases, and a row-level change is not automatically a stable public event contract.

4. Deliberately wrong approach: “CDC delivers exactly once, so duplicates are impossible”

Suppose a consumer processes source position 110, updates its search projection, but crashes before durably storing checkpoint 110. On restart its persisted checkpoint is still 100. Positions 110 and 120 may be delivered again. PostgreSQL 18's logical-decoding documentation explicitly warns that after a crash a slot can return to an earlier persisted position and resend recent changes; clients must avoid harmful duplicate processing.

The safe design records source position/event identity with the derived effect or makes the operation naturally idempotent. Do not confuse source-order replay semantics with end-to-end exactly-once effects.

5. AtlasMart lab: compare raw CDC to semantic events

Mandatory lab environment

Python 3.13+ standard library only; verified with Python 3.13.5. No PostgreSQL server, Debezium, Kafka, Docker, cloud account, privileged replication role, external network, or destructive log manipulation is required.

The simulator emits WAL-like changes, maps one transition to a domain event, records a snapshot boundary, and intentionally restarts from an older durable checkpoint.

python · AtlasMart deterministic simulation
from dataclasses import dataclass
from copy import deepcopy

@dataclass(frozen=True)
class WalChange:
    lsn: int
    txid: int
    table: str
    op: str
    key: str
    before: dict | None
    after: dict | None

wal = [
    WalChange(100, 41, "orders", "INSERT", "o-7", None,
              {"status":"CREATED", "total":120, "version":1}),
    WalChange(110, 42, "orders", "UPDATE", "o-7",
              {"status":"CREATED", "total":120, "version":1},
              {"status":"PAID", "total":120, "version":2}),
    WalChange(120, 42, "payments", "INSERT", "p-7", None,
              {"order_id":"o-7", "captured":True, "version":1}),
]

print("DATABASE-LOG CDC")
for c in wal:
    print(c.lsn, c.txid, c.table, c.op, c.key, "before=", c.before, "after=", c.after)

print("\nDOMAIN MAPPING")
def to_domain_event(change):
    if change.table == "orders" and change.op == "UPDATE":
        if change.before["status"] != "PAID" and change.after["status"] == "PAID":
            return {"type":"OrderPaid", "order_id":change.key,
                    "order_version":change.after["version"], "source_lsn":change.lsn}
    return None
for c in wal:
    evt = to_domain_event(c)
    if evt: print(evt)

print("\nSNAPSHOT + OFFSET")
source = {"o-7":{"status":"PAID","version":2}, "o-8":{"status":"CREATED","version":1}}
snapshot_at_lsn = 120
print("snapshot rows:", deepcopy(source), "resume_after_lsn:", snapshot_at_lsn)

print("\nCRASH / REPLAY")
consumer_checkpoint = 100          # last durable checkpoint
already_seen_in_memory = 110       # processed but not durably checkpointed
print("processed through", already_seen_in_memory, "but durable checkpoint is", consumer_checkpoint)
replayed = [c.lsn for c in wal if c.lsn > consumer_checkpoint]
print("after restart CDC may replay:", replayed)
print("duplicate-tolerant consumer required:", 110 in replayed)

print("\nSEMANTIC WARNING")
print("row UPDATE says what bytes changed; it does not by itself prove why the business changed them")
Expected evidence

The restart replays position 110 even though it was processed in memory before the crash. The output also demonstrates that only the application-level mapper knows that one row transition means OrderPaid.

6. Production judgment

Use log CDC when consumers need a faithful stream of committed source changes, when application modification is difficult, or when many derived systems must follow the same source. Use explicit domain events when the semantic contract matters more than physical schema and the application can own that contract. For CDC, observe source-log retention, connector checkpoint/offset, snapshot phase, lag in time and bytes, schema-history health, replication-slot/WAL retention, and duplicate/replay counts. Apply least privilege to log readers because CDC can expose sensitive columns that ordinary application APIs would filter.

Current optional references: PostgreSQL 18 provides logical decoding from WAL and durable replication slots; Debezium 3.6.1.Final (August 4, 2026) is the latest stable 3.6 maintenance release and supports PostgreSQL 18 among its tested versions. Neither is required for the mandatory lab.

The next lesson solves the application-side atomicity problem: how to commit business state and publication intent without a fragile database-plus-broker dual write.

Check your understanding

  1. Why is a row-level CDC record not automatically a domain event?
  2. What is the purpose of a CDC offset or LSN?
  3. Why can a consumer see the same change again after restart?
  4. What does an initial snapshot solve?
  5. When are domain events preferable?
Review the answers

1. It describes a storage/database change; business intent and stable semantic meaning require an explicit domain contract or mapping.

2. It identifies progress in the source stream so a consumer can resume/replay from a known position.

3. Its durable checkpoint may lag behind work processed before the crash; many CDC systems intentionally replay rather than lose data.

4. It establishes existing state while coordinating a source position from which subsequent changes can continue without an uncontrolled gap.

5. When downstream consumers need an intentionally owned semantic contract insulated from source schema details.

References

Foundational claims use primary specifications/research or current official documentation where practical. Product references are optional implementation anchors; the mandatory labs are vendor-neutral.

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.