The situation

Someone proposes “event sourcing” and the conversation immediately conflates three separate things: storing state as a log of events, publishing events for other services to consume, and streaming database changes downstream.

They have different costs and different payoffs. Separating them is most of the work.

The three things

Event sourcingEvent-driven integrationChange data capture
What it isYour state is the event log; current state is a fold over itServices publish domain events others subscribe toRead the database’s replication log, emit row changes
Source of truthThe event logEach service’s own storeThe database
Who defines the schemaThe domainThe publishing team, deliberatelyThe table structure, accidentally
Main benefitPerfect audit trail, temporal queries, rebuild any projectionDecoupling, extensibilityZero application change; get data out of a system you cannot modify
Main costVery high. Schema evolution on the log is foreverModerateCouples consumers to your physical schema
When it paysDomains where history is the product: ledgers, trading, complianceMost systems that have grown past two teamsAnalytics pipelines, migrations, legacy integration

The default

Use event-driven integration where you have publishers and consumers on different teams. Use CDC to feed analytics. Do not use event sourcing unless history is the product.

That last one is a strong claim, so here is the reasoning: event sourcing makes your event schema permanently load-bearing. You cannot fix a modelling mistake from year one, because rebuilding state requires replaying events written under the old model. You get versioned events, upcasters, and a growing pile of translation code that must be correct forever.

For a ledger, an insurance policy system, or anything regulated where “what did we know on 3 March and why” is a real question, that cost is worth paying — often it is cheaper than the audit machinery you would otherwise build.

For a typical CRUD product, it buys you an audit log you could have gotten from an audit table.

CDC: the underrated one

Change data capture is the least fashionable of the three and the most frequently correct. It reads the database’s own replication stream (Postgres logical decoding, MySQL binlog) and emits row-level changes.

graph LR
    A[(Service DB)] -->|"replication log"| B[CDC connector]
    B --> C[Topic: orders]
    C --> D[Analytics warehouse]
    C --> E[Search index]
    C --> F[Cache invalidation]
    style B fill:#fd6,stroke:#c90,color:#000

What makes it valuable:

  • No application changes. The service does not know it is happening.
  • No dual-write problem. The write to the database and the emitted event cannot diverge, because the event is derived from the committed write. This solves the single hardest problem in event publishing.

What makes it dangerous:

  • Consumers become coupled to your physical schema. Rename a column and you break the warehouse. This is the reason CDC output should feed an internal pipeline, not be published as a public contract.

If you want both — no dual-write and a clean public contract — the standard answer is the transactional outbox: the service writes domain events to an outbox table in the same transaction as the state change, and CDC (or a poller) ships that table. The event schema is yours to design; atomicity comes free.

The dual-write problem, stated plainly

Without an outbox or CDC, publishing an event after a database commit has an unfixable window:

sequenceDiagram
    participant S as Service
    participant D as Database
    participant B as Broker
    S->>D: COMMIT order
    D-->>S: OK
    Note over S,B: 💥 crash here
    S--xB: publish OrderPlaced
    Note over D,B: DB says the order exists.<br/>Nobody downstream knows.

No amount of retry logic closes this, because the crash can happen before the retry loop starts. Every “we publish after commit” design has this bug; most teams discover it during an incident rather than a review.

When the default is wrong

  • Small system, one team, no consumers. Skip all of it. An audit table and a nightly export is not technical debt at that size, it is proportionate.
  • You need to rebuild state at a past point in time for legal reasons. Then event sourcing is not over-engineering; it is the requirement.

What it costs

Any of these three adds a durable, schema-bearing, replayable stream to your system that will outlive the reason you added it. Plan for: schema evolution on messages, a dead-letter queue with an owner, replay tooling, and the observability to answer “where is this message” at 3am.

The broker is a weekend. The operational maturity is a year.

See also

Last updated 20 Aug 2026, 00:00 UTC. history