Skip to content
  • event-sourcing
  • architecture
  • payments
  • backend

Event Sourcing: The Database That Refuses to Forget

Most databases are designed to forget.

Every time you run an UPDATE, you destroy history. The old value is gone, overwritten, and the only thing your system remembers is where things stand right now. For a lot of software that’s fine. For some software, especially anything that moves money, it’s a quiet liability that you only discover on the day you need the history back.

Event sourcing is a pattern built on one idea: instead of storing the current state and throwing history away, store the history and derive the current state from it. It’s an old idea (accountants have worked this way for centuries), but it remains one of the most useful concepts in distributed systems, and one of the most misunderstood.

This article explains what event sourcing actually is, why it matters in domains like payments, and, just as importantly, what it is not. Because a lot of systems that people describe as event-sourced are nothing of the kind.

The state-oriented world you already live in

Here is a perfectly normal database record:

Transaction
  id:     12345
  amount: ₦100,000
  status: SUCCESS

It tells you where the payment is now. It tells you nothing about how it got there.

But this transaction had a life. Yesterday it looked like this:

status = PROCESSING

Then:

status = SENT

Then things got interesting:

status = TIMEOUT

And finally:

status = SUCCESS

Each transition happened through the same, familiar operation:

UPDATE transactions
SET status = 'SUCCESS'
WHERE id = '12345';

Every one of those UPDATEs overwrote the previous state. The row is a snapshot with amnesia. The journey from PROCESSING to TIMEOUT to SUCCESS, which is arguably the most interesting part of this transaction’s story, exists nowhere in your primary data model. Maybe fragments of it survive in application logs, if the logs were configured, retained, and searchable. Maybe.

This is the state-oriented model, and it is the default in almost every CRUD application ever written. Its core assumption is that the present is what matters and the past is disposable.

Event sourcing flips the model

Event sourcing takes the opposite position: the past is the source of truth, and the present is just a calculation.

Instead of updating a row, you append immutable events to a log. For our payment, the store would contain something like:

PaymentInitiated

PaymentValidated

AccountDebited

PaymentSubmitted

PaymentTimedOut

StatusQuerySent

PaymentConfirmed

Events are treated as immutable. Rather than updating past events, you append new ones that describe what happened next, and the event history becomes the authoritative record. Each event captures a fact that happened, in past tense, with a timestamp and whatever data describes it. The current state is no longer the authoritative source of truth. Conceptually it can always be derived by replaying the events in order, and in practice systems usually maintain projections and snapshots (more on those shortly) so they don’t replay the entire history for every request.

Conceptually:

Current State = f(all previous events)

If you replay PaymentInitiated through PaymentConfirmed and apply each event to an initially empty transaction object, you arrive at status: SUCCESS, amount: ₦100,000. The same answer the UPDATE-based system had. The difference is that you also still have everything else.

Three practical pieces make this workable in real systems:

The event store. An append-only log, usually grouped by aggregate (all events for transaction 12345 form one stream). This can be a purpose-built database like EventStoreDB, or a well-designed table in Postgres. Append-only is the property that matters, not the product.

Projections (read models). Replaying thousands of events to answer “what’s the status?” would be absurdly slow for queries. So you build projections: read-optimized views (often just regular tables) that are updated as events arrive. Your dashboard queries the projection. The event log remains the truth; the projection is a disposable, rebuildable cache of it. If you find a bug in how a projection was computed, you fix the code and rebuild it from the log. Try doing that with overwritten rows.

Snapshots. For long-lived aggregates with huge event streams, you periodically store a snapshot of computed state so replays only need to process events since the last snapshot. An optimization, not a requirement.

Put together, the flow looks like this:

   Command ("submit payment")


   Aggregate (validates, decides)


    PaymentSubmitted event


       EVENT STORE
    (append-only truth)

    ┌───────┼───────────────┐
    ▼       ▼               ▼
Transaction  Reporting   External events
projection   projection  out (Kafka etc.)
    │           │
    ▼           ▼
   API      Dashboard

The write side appends facts. The read side consumes them. Everything downstream of the event store can be thrown away and rebuilt from it.

Why payments people care

Everything above is abstract until a dispute lands on your desk.

A customer says: “You debited me but the beneficiary didn’t receive the money.”

In a state-oriented system, you have a row that says SUCCESS and a customer who says otherwise. Now you’re grepping logs across services, hoping retention policies were kind, trying to stitch together a story from fragments.

In an event-sourced system, you reconstruct the timeline directly from your primary data:

10:03:21  Payment initiated
10:03:21  Validation passed
10:03:22  Debit successful
10:03:22  Request sent to processor
10:03:52  Request timed out
10:04:01  Status query initiated
10:04:02  Scheme returned: successful

That timeline is not a nice-to-have. In payments it is the raw material for almost everything that happens after the happy path ends:

  • Disputes. You can show exactly what your system did and when, second by second.
  • Reconciliation. When your records and the scheme’s records disagree, the event history tells you which side of the timeout window each transaction fell on.
  • Debugging. “How did this transaction end up in this state?” stops being forensic guesswork. You replay it and watch.
  • Recovery. After an incident, you can recompute state from the log instead of hand-patching rows and praying.
  • Audit and regulation. Auditors and regulators often need to understand not just the current state but how it was reached. A well-designed event history provides much of that evidence, provided the surrounding controls around integrity, access, retention, and traceability are also in place.

Notice that the timeout in our example is not an error state that got overwritten and forgotten. It’s a permanent part of the record, sitting right there between PaymentSubmitted and StatusQuerySent, explaining why there was a 40-second gap and why a status query happened at all. In payment systems, timeouts followed by requery are not edge cases. They are Tuesday. A data model that erases them is hiding your system’s most important behavior.

The distinction most people get wrong

Here is where I need to be careful, and where a lot of engineers overclaim.

Using Kafka does not make your system event-sourced. Neither does RabbitMQ, asynchronous messaging, publishing domain events, idempotent consumers, retries, or the transactional outbox pattern. All of those belong to event-driven architecture, which answers a different question.

The distinction:

Event-driven architecture is about communication. Services notify each other that things happened, asynchronously, through events. The events are messages in transit. Once consumed, they’ve done their job. Each service typically still stores its state the ordinary way, as mutable rows.

Event sourcing is about persistence. The events are not just notifications, they are the system of record. State is derived from them. Delete the event log of a truly event-sourced system and you have deleted the system’s memory entirely, because there is no “real” state stored anywhere else.

A useful heuristic: if you deleted the integration events after they were consumed, could each service still reconstruct its authoritative business state from its own database? If yes, the system is probably event-driven but not event-sourced. In a truly event-sourced service, removing the event history removes the authoritative record itself, because the events are the state.

The two patterns compose beautifully (an event-sourced service often publishes its events outward for others to consume), but they are not the same thing, and describing an event-driven system as event-sourced is the kind of imprecision that unravels in a technical interview or a design review. Plenty of serious payment systems use Kafka, outbox tables, and asynchronous messaging everywhere and are not event-sourced at all. That’s not a deficiency. It’s usually the right call.

The costs nobody puts in the diagram

Event sourcing looks clean on a whiteboard. In production it makes you pay for that timeline, and the costs are real.

Event schema evolution. Your events live forever, but your code changes. The PaymentInitiated event you write today will be replayed by code you haven’t written yet, five years from now. Versioning events, upcasting old formats, and never breaking replay compatibility is a permanent tax on every change you make.

Eventually consistent read models. Projections lag behind the event log, usually by milliseconds, occasionally by more. Your UI and your APIs have to live with reads that may be slightly stale. Most teams underestimate how much design effort this consumes.

Deletion is hard by design. Immutability collides head-on with privacy regulations that grant a right to erasure. Architectural techniques help, such as keeping personally identifiable information out of domain events, or crypto-shredding, where per-user data is encrypted and the key is destroyed. But whether any of that satisfies a specific legal obligation depends on jurisdiction, implementation, and what lives in your backups. This quickly becomes a data-governance problem rather than merely an engineering one.

Querying is different. “Show me all transactions above ₦1M for this account” is trivial in SQL and awkward against an event log. You end up building and maintaining projections for every query shape you need.

It’s a paradigm, not a library. The team has to think in events: modeling them well, naming them in past tense, deciding aggregate boundaries. A team that hasn’t internalized this will produce an event log that is just a change-log of UPDATEs wearing a costume, with all of the complexity and none of the benefit.

What to do instead (most of the time)

For most systems, you don’t need full event sourcing to escape database amnesia. There is a spectrum, and the middle of it is underrated:

A status history table. Keep your normal mutable row, but also insert a row into transaction_status_history on every transition, with timestamp, old status, new status, and reason. It is a comparatively small change to an ordinary CRUD design, yet it alone may be enough to reconstruct the dispute timeline above. For anyone building payment flows, it’s the single highest-value habit on this list.

An audit log of domain events. Persist meaningful business events alongside your state, as a record rather than as the source of truth. You get the timeline without rebuilding your persistence model.

Change data capture. Tools like Debezium can stream your database’s own change log outward, giving you history and integration events without changing how your application writes data.

Full event sourcing becomes particularly compelling in domains where historical state transitions are central to the business, where replay and recomputation provide genuine operational value, or where temporal reasoning is a first-class requirement. Even then, it should earn its complexity. If a history table or an audit log solves the actual problem you have, use the simpler design. It will carry you a very long way.

The takeaway

Event sourcing is ultimately a statement about what your system considers true. State-oriented systems say the truth is where things stand now. Event-sourced systems say the truth is what happened, and “now” is just a view of it.

You don’t have to adopt the full pattern to benefit from the mindset. Start noticing where your UPDATEs quietly destroy information that someone (an auditor, an ops engineer, a customer with a dispute, or you at 2am) will eventually want back. Preserve the events that matter, whether in a full event store or a humble history table.

And keep the vocabulary honest. Using Kafka does not make you event-sourced. Making events your authoritative source of truth does. Knowing the difference, and knowing which one your system actually needs, is exactly the kind of judgment that separates using patterns from understanding them.

Isaac Olanrewaju is a backend engineer in Lagos, Nigeria, building payment systems, transaction-heavy services, and financial infrastructure for fintechs, banks, and product teams.