1

My understanding of a CQRS architecture is that a micro-service is split to two servers each with its own database such that the first server handles all the writes and the second server handles all the reads. Combining this architecture with event sourcing means that the write side will store events that keep track of state changes, and the read side will take all those events and replay them creating the state of the system at some point in time. How are we supposed to efficiently validate business rules?

An intuitive answer is just pull or query all the events of a specific entity from the event store and replay them then validate the business rules against that.

My problem with this answer is that replaying all the events on every command is a waste of processing resources. why not store that final object in a database and use it to validate business rules?

Well, if we add a database to the read side wouldn't we just end up with duplicate of the read model but now shares the same server and code base as the write side?

Another answer is use the read model for validation. With this particular solution, there are two problems.

The first one is the command handlers will have to be written in the query side instead of the read side. this doesn't make any sense and feels wrong. The second one is race conditions.

The read model communicates with the write side through a messaging queue. what if we have two consecutive commands such that the first one changes the state and the second one depends on the state change of the first one. in this case we might find ourselves in the situation where the first command is stored in the event store and transmits an event to the read model to update, but before the read model updates the second command was rejected or accepted based on stale, wrong, outdated data.

Is there no way to store the aggregate without ending up with a duplicate of the read model?

4
  • First commands should only commit, if they are a valid state change. A command failure is a valid state change, perhaps consider allowing a fixed number of retries? Second the Read optimised cache is not the source of truth, the event log is. If your client is caching state not yet received via the log it should be aware that it is fantasizing about an uncertain future, it is a no-no under this architecture. Third consider using workflows. Think of them as meta-commands with sequencing/parralelisation/fan out/fan in/on error/on success handling. Perhaps add transactions? – Kain0_0 Nov 25 '20 at 6:42
  • For write side, when using event sourcing you need to replay all necessary events in order to validate business logic. You can circumvent this to a certain degree by using snapshots, but you're still going to be working with events on top of that. This is why the event sourced part is used only for writes and not for reads - for which data is stored elsewhere. – Andy Nov 25 '20 at 14:39
  • Thanks to both of you @Kain0_0. How are we supposed to store snapshots? in the case of separate database we will have to query the separate database as well as the event store which i think is inefficient. storing the snapshot in the event store with all the other events will result in a problem, and that is how to query the snapshot and the events that come after it? – VOLK 14 Nov 25 '20 at 15:06
  • In one project I've worked on the reader/writer/event queue were all co-housed in the same database (but under different schemas) so it wasn't running into this issue. But it depends. The writer may generate snapshots to rebase the event stream onto so that it doesn't have to store so much data and can resync clients faster. In which case those snapshots are events. The clients may generate snapshots for the same reason, or to provide a better to query data structure. In which case the client stores them in its data-store (or not at all and rebuilds on restart). – Kain0_0 Nov 25 '20 at 22:13
2

Many people have been getting confused over what CQRS is. They look at CQRS as being an architecture; it is not. CQRS is a very simple pattern that enables many opportunities for architecture that may otherwise not exist. -- Greg Young, 2010

CQRS is "just" a pattern, the introduction of two "objects" where previously there was one. That is, in an of itself, a trade off (we're giving up the simplicity of a unified model to exploit other benefits), and one of the benefits is that we can choose different trade offs for writes than we choose for reads.

How are we supposed to efficiently validate business rules?

The usual answer is that we encode into the "write model" all of the checks and compensating actions to ensure that the history of events is internally consistent, which is to say that the history of events satisfies the business invariant.

The read model, with no authority to change anything, doesn't need to validate the rules. Our read models are usefully shaped transformations of the authoritative history.

My problem with this answer is that replaying all the events on every command is a waste of processing resources. why not store that final object in a database and use it to validate business rules?

So what you are really talking about here is caching -- and that's fine, but comes with trade offs.

In a stateful process, reloading the "final object" from disk over and over doesn't make a lot of sense, because you've already got a perfectly viable copy of that object in memory -- having a durable copy of the "final object" is only useful on startup. Furthermore, if you end up shutting down abnormally, it may make a lot more sense to abandon the in memory copy of the object and rebuild it.

You'll also see instances of a sort of hybrid approach, which combines a durable copy of the object from some point in the past along with more recent events.

In a stateless process, like a web server that can be processing many different requests independently, storing the object after each change can make a lot of sense. You do need to be careful about the failure modes that occur when you have multiple processes trying to write to the same resource - if your object representation and your event history happen to share the same coarse grained lock (ex: you are storing both in a relational database), then things get easier -- but of course that introduces additional constraints on your choice of storage.

Well, if we add a database to the read side wouldn't we just end up with duplicate of the read model but now shares the same server and code base as the write side?

Not necessarily -- there's going to be shared semantics (the information has the same meaning), but there's no particular reason that the processes that satisfy queries must be written in the same language as the processes that satisfy commands. For that matter, there's no particular reason that the processes that transform your write models to your read models need to be written in the same language as the other two.

The processes that are accessing the same data model need to have compatible semantics and schema, of course.

Your understanding of the issues of data races will be improved if you read through Pat Helland's Data on the Outside versus Data on the Inside.

Data on the inside is "our" data, the stuff we can lock against modification when we are computing how it should change. In a design where we separate writes and reads, this is the "write side".

Anything else is "data on the outside" - if we can't lock it, then we can't be sure that it is still true, only that it was true at some point in the past. And that in turn becomes a constraint on what we can and cannot do within a model -- our locked data should never conflict with itself, but it may conflict with information that is locked somewhere else. So in those cases we need to be thinking more in the language of detection, mitigation, escalation rather than prevention.

Memories Guesses and Apologies is a good introduction to this topic.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.