When People Mix Up Commands and Events

I can usually tell within the first ten minutes of a design conversation whether a team has really understood Event Sourcing. And no, it has nothing to do with which event store they picked, or how far along they are, or whether anyone in the room has read Greg Young.

The tell is much simpler than that.

Can they keep commands and domain events apart?

When they can’t, I stop asking about their architecture. Because whatever they tell me next isn’t going to mean much yet.

“But they’re both just records!”

Yes! On the surface they look identical. Both are small records with a name and a payload. Both get passed around the system. In C# both are usually a record. If all you had to go on was the shape of the type, you’d be forgiven for wondering what all the fuss is about.

The difference isn’t in the shape. It’s in what each one is allowed to do.

A command is a request. One recipient. It can be refused. And once it’s been handled, successfully or not, it’s gone. CancelOrder is a command. That order might already be on a truck somewhere, and the aggregate is fully entitled to tell you no.

A domain event is a fact. Any number of subscribers, none of whom get a vote, and it lives forever because it is the system of record. OrderCancelled is an event. You don’t get to argue with it. It already happened.

So here’s the ten-second check I use, and it sorts almost every confused message I’ve ever run into:

Who is allowed to say no?

Nobody? It’s an event.

Exactly one thing? It’s a command.

A subscriber? Congratulations, you’ve built a distributed bug, and you’re going to meet it at 3am on a Saturday.

What the mix-up actually looks like

It never shows up with a label on it. It shows up as a handful of small decisions that each seemed perfectly reasonable at the time.

An event that’s really an instruction. OrderShipped goes onto a queue that exactly one consumer is expected to act on, and the publisher cares whether that consumer succeeded. That’s a command wearing past tense as a disguise. WTF is that?

An event named after the command that caused it. Something like CancelOrderCommandProcessed. Now your log is coupled to a command that might get renamed, split in two, or deleted entirely next quarter. The cancellation is history. History doesn’t get renamed because you refactored a handler.

One marker interface over both. One bus, one pipeline, everything symmetrical. It feels tidy for about four months. Then nobody can say which records are the system of record and which ones were transient, and the answer is buried somewhere in dispatch code.

Commands written into the event store. Right next to the events. Replay now happily reissues intent that was already satisfied two years ago. Have fun with that one.

Every one of those is a symptom. The disease is that whoever wrote it hadn’t yet internalized that one of these things is a request and the other one is history.

Here’s what doesn’t work: explaining it again

This is the part I had to learn the hard way, and honestly it took me longer than it should have.

My first instinct used to be to explain the distinction more carefully. Better definitions. A sharper example. A nice diagram with arrows. Surely if I just say it clearly enough?

Nope. People nod, they agree it makes sense, and then they go right back to their editor where the two types still look identical and the compiler still doesn’t care one bit. A definition is cheap and it’s easy to agree with. Agreement is not understanding!

What actually changes someone’s mind is being made to sort real messages from their own business, out loud, standing up, in front of somebody who knows that business better than they do.

Which is exactly why I reach for Event Storming

If you’ve never run one: a long wall, a box of good sticky notes, no tables, no laptops, and the people who actually know how the business works. Alberto Brandolini invented it and it’s still the best two hours you can spend at the start of a project.

Domain events go on orange notes, in past tense, arranged left to right in the order they happen. Commands go on blue notes. And here’s the bit that does the teaching for me:

A blue note never gets a spot on the timeline.

Commands sit off to the side of the event they produced, next to the actor who issued them. They can’t go into the sequence, because the sequence is a story of what happened, and a request isn’t something that happened.

Somebody tries it anyway in the first hour of every single workshop I’ve run. They write “Cancel Order” on an orange note, walk up, and stick it on the wall. And within about a minute a domain expert says something like “well, that’s not what happened, that’s what the customer asked us for.”

That’s it. That’s the whole lesson, delivered by the right person, in the room, about their own business. It costs me nothing and it sticks in a way my careful explanation never did.

And the wall keeps paying after that. When the group can’t agree whether something is a fact or a request, that argument goes up as a red note, and red notes are the most valuable things in the room. Every single one is a question the team would otherwise have discovered as a production bug six months later.

Then when you’re done, the translation is almost mechanical. Orange notes are your candidate domain events. Blue notes are your candidate commands. The yellow ones are the actors your task-based UI is going to serve. You didn’t just teach the distinction, you walked out with the first draft of the model.

So

Mixing up commands and events isn’t a style thing you fix in code review. It’s a signal that the core idea hasn’t landed yet, and no amount of patient re-explaining is going to land it.

Put the team at a wall with somebody who knows the business, and let the business tell them the difference.

Have you run into this on your own teams? Which shape did it take for you? Drop a comment below, I’d love to hear about it.

Leave a comment