The second context check still seems to suffer from a race condition:
Portal 1: transfer money from Alice to Bob
Portal 2: transfer money from Alice to Charlie
Command processor 1: Alice's and Bob's accounts look consistent
Command processor 2: Alice's and Charlie's accounts look consistent
CP 1: check again, still good
CP 2: check again, still good
CP 1: complete the transfer from Alice to Bob
CP 2: complete the transfer from Alice to Charlie
But now Alice is overdrawn.
Maybe as a first step, even before the initial query, the command processor issues a "request to transfer" event, and then, during "check again", they check for new events after their own "request to transfer", and also check for other, active "request to transfer" events.
The conditional append - "check again" plus append - is atomic. Hence if CP2 should be faster for some reason it finds the event store unchanged and append its events. When CP1 starts the conditional append the event store has changed.
Hey, great article and an interesting concept, but I have some practical questions about this approach.
Assuming that there’s a single “event stream” (append only log), it would render the querying step basically into a O(n), that grows with each new event appended.
It must be run every time we call a command and not only that, but we need to run this twice – second time before writing new events to make sure we don’t have a stale model.
If we were to introduce basic optimization used in ES – namely, snapshots, we would get another problem: in old Aggregate approach we had single model per each entity. In the new approach we must store Models per each Command Context and per each entity (e.g. account/device) which is very inconvenient.
Each command handler or slice can do whatever it finds appropriate to optimize performance.
- Contexts can be cached and updated as needed.
- The event stream can be queried upon conditional-append starting from a certain version/event forward.
If commands want to share contexts they of course can do some. But that's a conscious optimization, not a premature one like aggregates are. (Aggregates are premature optimization because you design them before you know all the commands affecting them. If you wait with optimization as the set of events grows and the number of commands increases, you can optimize for specific patterns you detect.)
That’s the point - how can a handler NOT require optimization on a typical production environment with fairly large events number? Given that we have a single log and the needed event type can be at the start of event log, we have to either process everything from the start or use caching/snapshotting, so that optimization is pretty much inevitable?
Ralf,
The second context check still seems to suffer from a race condition:
Portal 1: transfer money from Alice to Bob
Portal 2: transfer money from Alice to Charlie
Command processor 1: Alice's and Bob's accounts look consistent
Command processor 2: Alice's and Charlie's accounts look consistent
CP 1: check again, still good
CP 2: check again, still good
CP 1: complete the transfer from Alice to Bob
CP 2: complete the transfer from Alice to Charlie
But now Alice is overdrawn.
Maybe as a first step, even before the initial query, the command processor issues a "request to transfer" event, and then, during "check again", they check for new events after their own "request to transfer", and also check for other, active "request to transfer" events.
The conditional append - "check again" plus append - is atomic. Hence if CP2 should be faster for some reason it finds the event store unchanged and append its events. When CP1 starts the conditional append the event store has changed.
Does it mean the event store needs to support the concept of a single-operation atomic check+append?
Hey, great article and an interesting concept, but I have some practical questions about this approach.
Assuming that there’s a single “event stream” (append only log), it would render the querying step basically into a O(n), that grows with each new event appended.
It must be run every time we call a command and not only that, but we need to run this twice – second time before writing new events to make sure we don’t have a stale model.
If we were to introduce basic optimization used in ES – namely, snapshots, we would get another problem: in old Aggregate approach we had single model per each entity. In the new approach we must store Models per each Command Context and per each entity (e.g. account/device) which is very inconvenient.
How would you tackle this?
Each command handler or slice can do whatever it finds appropriate to optimize performance.
- Contexts can be cached and updated as needed.
- The event stream can be queried upon conditional-append starting from a certain version/event forward.
If commands want to share contexts they of course can do some. But that's a conscious optimization, not a premature one like aggregates are. (Aggregates are premature optimization because you design them before you know all the commands affecting them. If you wait with optimization as the set of events grows and the number of commands increases, you can optimize for specific patterns you detect.)
That’s the point - how can a handler NOT require optimization on a typical production environment with fairly large events number? Given that we have a single log and the needed event type can be at the start of event log, we have to either process everything from the start or use caching/snapshotting, so that optimization is pretty much inevitable?
Sure, in the end optimization is inevitable. A cached context model is an example.
But optimization is technical, it‘s orthogonal to the concept. It should not be part of the paradigm.
Remember the relational calculus. Check if indexes are part of it. I don‘t see them in the description of the relational paradigm.
They are an implementation detail. No need to understand them to start working with SQL.
Sure, in the end you want to define them wisely and even rearrange your SQL for optimal use. Still that‘s optimization outside the paradigm.