~/architect
#event-sourcing #event-driven #architecture #distributed-systems #ddd

Demistifying Event Sourcing (2 of 5): same name, very different beasts

Event sourcing and event-driven architecture share a word and almost nothing else. One is a persistence strategy, the other a communication style, and conflating them leads to bad designs.

The second myth in this series is the one I see most often in design reviews: a team claims to be “doing event sourcing” because their services publish events on Kafka, or a team rejects event sourcing because “we already have an event-driven architecture.” Both statements collapse two patterns that solve completely different problems. This article untangles them, and then shows the one place where they genuinely connect.

Myth 2: “Event sourcing and event-driven architectures are the same thing”

The Myth: “We publish domain events to a broker, so we’re already event sourced.”

The Reality: Event-driven architecture is about how services talk to each other. Event sourcing is about how a single service stores its state. You can have either, both, or neither, and the decision criteria are entirely independent.

The confusion is understandable. Both patterns use the word “event,” both treat events as first-class citizens, and both tend to show up in the same kind of system. But the concerns they address sit on different axes.

Two patterns, two axes

Event-driven architecture (EDA) is a communication style. It governs how independent services exchange information: a producer emits an event to a broker, one or more consumers react. The producer doesn’t know who listens. The pattern lives at the boundary between services.

Event sourcing (ES) is a persistence strategy. It governs how a single service stores the state of its aggregates: instead of overwriting a row, the service appends an immutable event to a stream. The pattern lives inside one bounded context, behind the repository port (see part 1 for the implementation detail).

// Two patterns on two different axes
flowchart TB
subgraph EDA["Event-Driven Architecture (between services)"]
  direction LR
  SvcA["Service A"] -- "OrderPlaced" --> Broker[("Broker")]
  Broker -- "OrderPlaced" --> SvcB["Service B"]
  Broker -- "OrderPlaced" --> SvcC["Service C"]
end

subgraph ES["Event Sourcing (inside one service)"]
  direction LR
  Cmd["Command Handler"] --> Agg["Aggregate"]
  Agg -- "records" --> Store[("Event Store")]
  Store -- "replay" --> Agg
end

A service can be event sourced and never publish anything to anyone. A service can publish a torrent of integration events to Kafka and store its state in a perfectly ordinary relational table. The two decisions are orthogonal.

The events are not the same events

This is the part teams miss. The events flowing through a broker and the events sitting in an event store are different artefacts with different responsibilities:

AspectES domain eventEDA integration event
AudienceThe aggregate itself, on replayOther bounded contexts
GranularityEvery state change, including internal onesCoarse-grained, business-relevant facts
Schema stabilityVersioned within the service, evolves with the modelA public contract, evolves carefully
OwnershipDomain layerAnti-corruption / published language

A ReviewScoreRecomputedEvent makes perfect sense inside the rating aggregate’s stream — it’s part of how the aggregate reconstructs itself. It has no business leaving the service. Conversely, a CourseRated integration event carries the curated, stable shape that downstream services depend on. The two often coexist, but treating them interchangeably leaks internals into your public contracts and freezes refactors you should have been free to make.

Where they meet: publishing events without the outbox

There is one genuine and useful overlap, and it’s the reason the two patterns get conflated in the first place. Event sourcing eliminates the dual-write problem that EDA usually has to solve with the outbox pattern.

In a non-event-sourced service that participates in EDA, every state-changing operation has to do two things atomically: update the database and publish an event. Two systems, no shared transaction. The standard fix is the transactional outbox, often implemented with Debezium CDC — write the event to an outbox table in the same transaction as the state change, then relay it asynchronously.

Event sourcing collapses that ceremony. The event is the state change. A single append to the event store is simultaneously the commit of the business operation and the durable record that needs to be published. There is no second write to coordinate.

// Dual-write vs event-sourced publication
flowchart TB
subgraph Classic["Classic EDA service (dual write)"]
  direction LR
  H1["Handler"] --> DB[("State table")]
  H1 --> OB[("Outbox table")]
  OB --> Relay["CDC / Relay"]
  Relay --> Br1[("Broker")]
end

subgraph Sourced["Event-sourced service"]
  direction LR
  H2["Handler"] --> ES[("Event Store")]
  ES --> Pub["Projection / Publisher"]
  Pub --> Br2[("Broker")]
end

The publisher still has a job — it tails the event store and forwards selected, possibly transformed events as integration events — but the consistency guarantee comes from the store itself, not from a coordination dance.

A minimal Postgres-backed append captures the idea:

INSERT INTO event_store (
    stream_id, version, event_type, payload, occurred_at
) VALUES (
    $1, $2, $3, $4::jsonb, now()
);
-- Unique constraint on (stream_id, version) gives us optimistic concurrency.
-- This single statement is the commit of the business operation
-- AND the durable record to be published downstream.

That is the whole transaction. There is no UPDATE review_entry SET ... followed by INSERT INTO outbox .... The publisher subscribes to the event store and forwards what other services need to see.

Trade-offs

AdvantageDrawback
EDA without ES (outbox + CDC)Works with any storage model; existing services can opt in incrementally; no replay or schema-evolution machinery to buildTwo writes to keep consistent; relay infrastructure to operate; outbox table grows and needs pruning
ES without EDAAtomic state + history in one append; full audit and replay; no broker on the critical pathLimited value if no one consumes the history; queries need projections; team has to learn the pattern for a payoff only the owning service sees
ES + EDASingle-write publication; integration events derived from a durable, ordered source of truth; replayable for new consumersMust explicitly translate domain events into integration events; broker, projections and event store all need to be operated

The takeaway

Event-driven architecture and event sourcing answer different questions. EDA asks “how do these services talk?”. ES asks “how does this service remember?”. Treating them as the same thing leads to two failure modes: teams adopt ES because they want async messaging (and pay the storage complexity for no reason), or teams reject ES because they “already have events” (and end up reinventing audit logs and outboxes).

The honest connection is narrow but real: when you do want both, event sourcing gives you the publication side almost for free. The event store is the outbox. That is the one sentence worth remembering from this article.

Coming up in this series:

  • Myth 1: Implementing event sourcing is hard
  • Myth 2: Event sourcing and event driven architectures are the same thing —> you are here.
  • Myth 3: Storing every change forever will kill performance
  • Myth 4: If I change my data structure, old events are broken forever
  • Myth 5: It is difficult to query the state when using event sourcing

Next up: Myth 3 will tackle the performance question. Storing every change sounds expensive, and at some scale it is — but the mitigations are well understood and far less exotic than the folklore suggests. See you there.

← back to posts