domin
domin

Reputation: 1314

Composing business logic in an event-sourced system?

In my understanding, executing a command in an event-sourced system works as follows (simplified):

  1. Validate the command DTO itself. Reject if necessary.
  2. Load/hydrate the affected aggregate by applying events in order (projection).
  3. Validate whether or not the command can be applied from a business perspective by cross-checking the command with the state of the aggregate. Reject if necessary.
  4. Perform business logic on the aggregate. The aggregate itself remains unchanged, so the methods called on/with it just output events which log what happened.
  5. Persist the generated events atomically in the right order.
  6. Return to client.

I am interested in the part that performs the business logic. In a non-event-sourced system this is quite simple: Call methods that change the state of the aggregate in a way that maintains its invariants. We can easily chain and compose method calls, and easily interchange read and write operations. E.g. after having called a method that changed the state, we can call another one that acts on the new state already. We can do this all locally in one logical transaction.

In an event-sourced system I see no easy way to compose these calls. The first one might emit some events, but the second one does not yet see the effects of these events. This basically means that in the context of one command handler, I can only read once, and then write once. I cannot do a back-and-forth like I'm used to in standard OOP.

I can see some alternatives, each with their own drawbacks:

So how is business logic composition usually done in an event-sourced system?

Upvotes: 0

Views: 320

Answers (2)

Roman Eremin
Roman Eremin

Reputation: 1451

In our reSolve framework we end up with one command - one event scheme. Yes, it is a limitation, but it seems the only way to ensure consistency in the concurrent distributed environment. Multi-event logic in this case is performed by sagas.

We may allow to write several events, but, as you correctly mentioned, you cannot evaluate the state between these events, and you cannot/should not write them one-by-one, because some other command can change the aggregate state in between.

Upvotes: 1

Levi Ramsey
Levi Ramsey

Reputation: 20561

I would not say that applying the projection logic in the command handler is bloating the business code with infrastructure concerns. For an event-sourced aggregate, the core projection logic (a function, in Scala notation, of type (A, Event) => A, viz. which takes an aggregate and an event and returns a new aggregate) is the essential part of the aggregate and not an infrastructure concern.

So if such an aggregate has a method (partial Java notation) A applyEvent(Event e), and the command handler is accumulating a sequence of events, folding over the sequence to get a new state (which is duplicating the operation of the projection) doesn't strike me as bloating at all (admittedly, event sourcing is in some sense sneaking functional programming into enterprise development (there's a lot of mechanical sympathy with the free monad), so doing this in a language without some "lambda calculus nature" may be painful).

Setting that aside, you note that diff events are very low level. Event sourcing tends to favor high level events: emitting "a lot" of events for a command is perhaps a sign that the events want to be higher level descriptions of what (and why) the change happened. It's OK to have events that get handled in nearly identical ways (e.g. for a message board, a UserDeletedPost event and a ModeratorDeletedPost event will likely both be interpreted the same way by the Post aggregate, but there's a pretty substantial semantic difference between the events) and to have some repeated checks in command handlers. A tendency in event-sourced systems to have simple aggregates also supports this approach.

In some cases, a particularly coarse-grained command can be decomposed into multiple commands (effectively a saga). If an aggregate is going to support such coarse-grained commands, it may need to be modified to support an illusion of atomicity for the coarse-grained command. Event-sourcing implementations which map to the actor model can sometimes simplify this.

Upvotes: 1

Related Questions