Skip to content

Concepts

PostgreSQL Is Not an Event Store

The table takes about fifteen minutes to write. An ID, a stream name, a version number, an event type, a JSON payload, a timestamp. Add a unique constraint, add an index, and you have somewhere to put your events. It is the first thing almost every team reaches for, and for good reason: the database is already running, already backed up, already monitored, and everybody on the team can read it.

In the previous post we pinned down what an event store actually has to do and held Kafka against that list. This one turns it around and points it at the tool people reach for from the opposite direction – the general-purpose database they already trust with everything else. PostgreSQL passes more of that list than Kafka did, and that is exactly what makes it the harder case. Kafka's central failure announces itself. PostgreSQL fails without a sound, a year in, when a read model is missing a row that nobody can account for.

Your Progress Bar Doesn't Belong in the Event Store

The progress bar works. It fills up, users can see how far along their contract review is, and nobody has complained. Then you open the event store and find 312 events for a single contract, 310 of which nobody would have written if that bar did not have to move.

Nothing is broken, and that is exactly what makes this hard to argue about. The system does what it should, the event names speak the language of the domain, and every one of those events was written on purpose. The unease is real anyway, and if you can't say where it comes from, the discussion ends before it starts. So: most of those events have a shelf life, and the event store is the one place in your system that doesn't.

Compensating Events Won't Save You

A customer calls about their account statement. One line records a payment of 4,800 euros, and nobody at your company can explain where it came from. You look into the event store, and there it is: payment-received, timestamped, hash-chained, immutable. The event is wrong, and it will be wrong forever.

Ask around and you will hear the same answer everywhere: write a compensating event – a new event that reverses the effect of an earlier one, the way a refund reverses a payment. It is good advice, and for two of the three situations that produce a wrong event, it is exactly right. For the third, it makes things worse – and the third is the one nobody warns you about.

You Don't Need an Audit Log

Sooner or later, every serious system meets an auditor. Compliance, regulation, or plain good governance shows up and asks for the same thing: a complete, ordered, tamper-proof record of what happened and when. And almost every architecture review answers with the same reflex. Add an audit log. It has the right name, it produces something you can point at, and it feels responsible. In most systems, it is also the wrong answer.

This is not a case against auditing. Auditing matters, often more than the features built around it. It is a case against the separate audit log as the way to deliver it, because a good auditor, one who actually does the job, will take about one meeting to see through it. The reason they can is the same reason the audit log was compromised from the start, and it reaches all the way down to how you decided to store your data in the first place.

Kafka Is Not an Event Store

Every time a team sits down to build an event-sourced system, someone asks the same question: we already run Kafka, and Kafka is an append-only, ordered log, so isn't that exactly what Event Sourcing needs? The resemblance is real, and it is tempting. It is also superficial, and mistaking one for the other tends to get expensive months later, once the system is in production and the workarounds have quietly piled up.

So rather than arguing that Kafka is bad, because it is not, we want to do something more useful. We will first pin down what an event store actually has to do, then hold Kafka against that list and see where it stands. This is the first post in a small series that asks the same question of one candidate at a time. And for Kafka, the answer is the one that surprises people the least once they can see it laid out: it is an excellent tool, just not for this particular job.

You Don't Need an Outbox

The outbox pattern has quietly become canonical in microservice tutorials. It has a name, library support, conference talks, and a steady stream of blog posts that walk through implementing it. It's blessed. And yet, every time it shows up in a system, it's a sign that something else has gone wrong upstream.

The pattern itself is clever. It works around a real, structural problem: two systems that need to agree but can't share a transaction. The question worth asking is whether you need to have that problem in the first place. Once you take that seriously, the outbox stops looking like a pattern and starts looking like an admission of defeat.

Commands Aren't Just Events in Reverse

BorrowBook and BookBorrowed. ReserveSeat and SeatReserved. AcquireBook and BookAcquired. The first half is what someone wants, the second half is what happened, and the only visible difference is the tense. Most diagrams in most Event Sourcing tutorials draw the two as a pair, and after enough examples, your brain quietly starts assuming they always will be.

That picture gets people started, and it isn't wrong, exactly. But the more real your system becomes, the less it matches the shape of what's happening underneath. Commands and events look symmetric on the surface, and they're not. They're not symmetric in whether they can fail. They're not symmetric in who they speak to. They're not even symmetric in number. Treating them as if they were is one of the quietest, most expensive mistakes we see, because the code keeps working long enough for the asymmetry to set in everywhere before anyone notices.

Email Uniqueness in Event Sourcing

Recently, a reader asked us how to enforce email uniqueness when registering users in an event-sourced system. It is a fair question. In a relational database, you add a column, slap on UNIQUE, and the problem is solved. The database does the heavy lifting, and you move on with your day. In Event Sourcing, that easy answer simply does not exist.

The good news is there are several reasonable ways to solve it. The bad news is that none of them is free. Each one pays for uniqueness somewhere, whether in scale, in latency, in user experience, or in operational complexity. Choosing among them is not a technical question. It is a question about what your business actually means by "unique."

Sagas vs Process Managers

The moment a workflow spans more than one aggregate or service, someone will say: "We need a saga." It has become the default term for anything that coordinates multiple steps in an event-driven system. Book a flight, reserve a hotel, charge the credit card, and if any step fails, roll everything back. That's a saga, right?

Not quite. What most people describe when they say "saga" is actually a different pattern with a different name, different responsibilities, and different trade-offs. The confusion is not just semantic. It leads to architectural decisions based on the wrong mental model, and those decisions are surprisingly hard to reverse.

Decide, Evolve, Repeat

Almost every Event Sourcing implementation starts with aggregates. You define a class, give it a method for each command, mutate internal state when events are applied, and wire it all up with a framework. This works. Countless systems have been built this way. But if you step back and ask what Event Sourcing actually needs at its core, the answer is surprisingly minimal.

The Decider pattern, introduced by Jérémie Chassaing, strips Event Sourcing down to three functions. No classes. No inheritance. No framework. Just three pure functions that capture everything an event-sourced component does. It is one of the most elegant ideas in the Event Sourcing space, and once you see it, it changes how you think about the entire approach.