How to expose entity state via read-side?

(Ivan Oreskovic) #1

Lets assume I have a PersistentEntity whose command/event/state can be modeled by the following case class:

case class MyCommand(number: Int)
case class MyEvent(number: Int)
case class MyState(maxNumber: Int)

The entity should do a simple thing, keep the highest number it encountered so far.

What an entity does when receiving MyCommand is to generate MyEvent, which in turn updated MyState if number > maxNumber.

I would like to expose that state via read side, and from what I understand (I’m still a newbie when it comes to Lagom), in my implementation of ReadSideProcessor[MyEvent] I should describe how to react on those events via ReadSideHandler[MyEvent].
But the problem is that can only react on event changes, while I would like it to react when my state changes.
This leads me to think I should expose my state changes as events as well.
I was thinking how to do that, and what first came to my mind seems like an anti-pattern:

In MyCommand handler, also persist an event MyEvent2(maxNumber), where I have to calculate it from current state and number in MyCommand. The issue I see here is that the same business logic that takes place in MyEvent handler will take place here as well. I kinda don’t like that too much.
This is effectively calculating new state in Command handler and persisting entity state as such, which seems like an anti-pattern to me. This may be a simple example, but the real world scenarios might not be.

I’ve seen the blog example on Lagom docs, but the thing is that my state is, in lack of a better word, destructive. And it may be heavy to calculate, so I don’t want to do that in more than one place (entity).

I know Lagom snapshots entity state every now and then, when it deems it right, and I was wondering if I could do the same in some fashion.

I hope I didn’t complicate the example much :)

(Ignasi Marimon-Clos) #2

Hi @ioreskovic,

“Events” are “State Changes”. Or, put another way, your state will only change if there’s an event reflecting that change. Let me rephrase what you said:

  1. when a command comes, if the current state is that which accepts the incoming command (e.g. the maxNumber in the state is less than the number in the command) then an event is emitted. The event is emitted because we want to keep track of a state change ("the new maxNumber is number).
  2. the event is persisted in the database.
  3. after persisting, the event is applied into the state producing a new state with the updated maxNumber.

So the JOURNAL in the database will be a sequence of events containing an ever increasing list of number's. Then, your read side processor will consume those events (which reflect state changes).

Note that on step 1. if the number in the state was one which forbid the processing of the command (e.g. the number in command is less than maxNumber in the state) then no event would be emitted and an error stating the rejection of the command could be raised (you might also not raise an error and silently accept the command not producing any event, aka state change).

Hope this helps,

Cheers,

(Ivan Oreskovic) #3

Thank you @ignasi35, I very well understand that, but I’ve made a mistake by making my example a bit too simple, so it implies this solution.

Let me try to expand it a bit.

case class RunNumber(num: Int)
case class NumberRecorded(num: Int) // this is questionable
case class NumberStats(min: Int, max: Int, range: Int, sum: Int, count: Int, avg: Double)

Assuming our initial state for that entityId is None, let’s run a few commands:

  1. RunNumber(10) -> Some(NumberStats(10, 10, 0, 10.0, 1, 10.0))
  2. RunNumber(-5) -> Some(NumberStats(-5, 10, 15, 5.0, 2, 2.5))
  3. RunNumber( 1) -> Some(NumberStats(-5, 10, 15, 6.0, 3, 3.0))

And what I’d like to end up with in read-side is:

EntityId MinNum MaxNum Range Count Sum AvgNum
SomeId -5 10 15 3 6 2.0

As you can see, my command/events change parts of the state, so I cannot just disregard them, like it was in the case where only max was in play.

So, my commands are not idempotent.
One thing that comes to my mind is that on each RunNumber command I should emit a series of events which each record idempotent change in state.

So, for commands up there, I’d emit:

  1. List(MinChanged(10), MaxChanged(10), RangeChanged(0), SumChanged(10), CountChanged(1), AvgChanged(10.0))
  2. List(MinChanged(-5), RangeChanged(15), SumChanged(-5), CountChanged(2), AvgChanged(2.5))
  3. List(SumChanged(6), CountChanged(3), AvgChanged(3.0))

But that again forces me to calculate, for example, new average (which I consider business logic), in command handler.
On top of that, when I’d generate events for 3rd command, they’d be done like this:

RunNumber(n) -> SumChanged(state.sum + n)
RunNumber(n) -> CountChanged(state.count + 1)
RunNumber(n) -> AvgChanged((state.sum + n) / (state.count + 1))

As you can see, the logic is duplicated here (yes, it can be extracted to end up in function calls, but still, smells a bit to me :slight_smile:)

Instead of having, let’s say:

case class SetNumItemsInCartTo(item: String, n: Int)

I have:

case class AddNumItemsToCart(item: String, n: Int)

And in both cases I’d like to expose cart state via read side.
If I could use idempotent commands, it would be a breeze, simply emit
NumItemsRecorded(item: String, n: Int)
and be done with it. But to emit a similar event on command with non-idempotent command, I’d have to do business logic (adding items to card, that is, calculating state change) in my command handler, instead in my event handler.

I hope I’ve made my problem a bit clearer. :slight_smile:

(Alan Klikic) #4

Hi,

If , in you case, it is not required to emit mutiple events per RunNumber command I would suggest using only one event holding mutated NumberStats:

case class NumberRecorded(num: Int, stats: NumberStats) 

NumberStats would stored in the MyState.
MyState would expose prepareMutation function where all business logic for calculating/mutating NumberStats takes place. Also update function to update NumberStats by an event handler :

def prepareMutation(num: Int): NumberStats = ....
def update(stats: NumberStats): MyState = ....

In command handler, if RunNumber command is valid, NumberRecorded event is emitted with num from RunNumber (indicating what number did the mutation) and mutated NumberStats from prepareMutation.
Event handler for NumberRecorded would do the update NumberStats in MyState.
With this approach you NumberRecorded event would be autonomous.

Just a note that this approach is not for all use cases. For example it would be sub-optimal if NumberStats metadata is rather big in size.

Hope this helps.

Br,
Alan

(Ivan Oreskovic) #5

Thanks (again) Alan! :smiley:
I thought about that approach, but I wasn’t able to dissect it the way you showed me.

Cheers!