johanhaleby / occurrent

Unintrusive Event Sourcing Library for the JVM
https://occurrent.org
120 stars 16 forks source link

Added EventSourcedDecider / StateDecider #158

Closed johanhaleby closed 6 months ago

johanhaleby commented 6 months ago

Create a decider impl that takes an AS and a decider and returns a decider (that maintains the semantics of the decider) that loads and stores the events. Something like:

fun eventSourcedDecider(decider, applicationService, functionThatResovlesStreamIdFromCmd) {
   initialState = decider.initialstate
  decide = .. 
}
bartelink commented 6 months ago

functionThatResovlesStreamIdFromCmd

why not specify the target indepdendently of the command (IME embedding this into every command case is just busywork for no value; the code is clearer if the ids and/or context that dictates the target is passed as its own thing and/or enriched/resolved as it passes through layers)

johanhaleby commented 6 months ago

Thanks for checking in @bartelink :) 👋

I mainly wrote this in a hurry for myself to remember later, but I'm very curious about your take. My thinking is that you need some way to derive which streamId to store the events generated by the decider, and that this id must be derivable from the command somehow. My thinking with this issue was to create a decider that wraps infrastructure logic (AS), and since the AS impl in Occurrent requires as streamId, my thinking was that you need to pass some sort of function that takes a Command and return a StreamId for this command when creating the decider. Maybe this is a bad idea, I don't know, but I just thought about it randomly today and quickly wrote it down here to explore it more later :)

bartelink commented 6 months ago

It goes with the identity section in https://nordfjord.io/equinox (see longer walkthrough in https://discord.com/channels/514783899440775168/762671777372962837/1210884086256635914)

For me 'the command' is 'the invocation'; you are serving a request. As part of that you'll have various stages of vaildation and enrichment

e.g. the initial request might be contextless. Then you look at the identity and determine that through some delegated role you'll be acting as some other principal. And the specific configuration for the tenant will then dictate the ultimate database in which the stream lives.

If all you have is a union where every single case has an id value and a function to unpack it, one size must fit all

So I'm suggesting that the routing (deciding the tenant/principal/context) should be separated from the material parts of the request that are specific to a given decision

If you follow that, then you don't have/need such a boilerplate function that needs to uncouple that which should not have been coupled in the first instance.

Whether or not you ultimately put some contextual info (the requestor, who it's on behalf of, and the tenant id etc) into the generated events is also separable from the decision logic

In short, forcing everything into the straitjacket of "pack it all into this command DU" might appear Easy, but it's not Simple.

johanhaleby commented 6 months ago

Thanks for clarifying.

The command that I have in mind here is an "internal" command, i.e. it has been "enriched" with whatever is needed. In this context, I see the command as a substitute for method invocation. My idea here was more along the lines of: "Would it be possible to create a single decider for your entire application, which also encapsulates infrastructure?" I.e. let's say I receive an HTTP request, then I just get a hold of my single "master decider" (via dependency injection or something), which encapsulates infrastructure stuff (internally) and knows how to load and save events when you just pass it the command (that you've created from the contents of the HTTP request).

If you can combine these kinds of deciders and maintain the decider semantics, you should be able to get a single decider that can load/save events from different event stores transparently (as an example). I just thought that this might be a cool idea to try out, I have no idea if it's feasible or valuable :). For the decider to know how to load the events, it needs some form of routing from a command to a streamId. And given that I just created the command as a data structure after having received an HTTP request, the functionThatResovlesStreamIdFromCmd function would be the "routing table". I.e. something that figures out the streamId for every command. And it should be composable as well I presume. It's something you would define once when creating the "master decider", and not for every invocation of the decider 🤷‍♂️

bartelink commented 6 months ago

Hm would not a fan of that. Binding all decisions and all result types associated with each of those for aggregates is already plenty coupling. You just end up with a Gordian knot where the most basic unit test pulls the banana with all the monkeys out of the jungle

Having a good set of library level abstractions that work with a concrete store and a memory store is one job integrating the whole thing with an app FW or DI story is something that can and should be separated (definitely internally for your own sanity, but IMO also from the POV of explaining and maintaining the offering as a whole)

I can definitely imagine being able to do something neat in the context of an app, but you don't want to bake that complexity and implied coupling into a lib.

In other words, it does not sound like you're delivering composable pieces; for me having the composition driven externally is key to maintaining Simplicity in the Rich Hickey sense of the word.

johanhaleby commented 6 months ago

Yeah, I'm not sure I would be a fan of this either, it was a random thought that popped up a couple of days after watching Jérémie Chassaing's talk at DDD Europe that I wanted to get down :) And sure, it might be taking things too far and complect too many different things.

However, I never for a second had in mind that this was something that I would force to users of Occurent. DI, as I mentioned earlier, is not something I'm building or requiring in Occurrent.

Here's some Kotlinish code of what I meant (ignore the naming of events/commands etc, that's not what's important here):

sealed interface Command

sealed interface InvoiceCommand : Command
data class CreateInvoice(val invoiceNumber: Int) : InvoiceCommand
data class PayInvoice(val invoiceNumber: Int) : InvoiceCommand

sealed interface BulbCommand : Command
data class Fit(val maxUses: Int) : BulbCommand
data object SwitchOn : BulbCommand
data object SwitchOff : BulbCommand

Now in your code, when receiving e.g. an HTTP request, you can "fit a bulb" like this:

val eventStore = .. 
val cloudEventMapper = ..
val applicationService = GenericApplicationService(eventStore, cloudEventMapper)

val invoiceDecider = decider<InvoiceCommand, InvoiceEvent>(...)

post("/bulbs/{id}") { id, maxUses ->
    val streamId = "Bulb:$id"
    val cmd = Fit(maxUses)
    applicationService.execute(streamId, cmd, bulbDecider)
}

My idea was just this, you first create some "routing table" for both invoice and bulb commands:

fun invoiceCmdToStreamId(cmd : InvoiceCommand) : String = "Invocie:" + when (cmd) {
    is CreateInvoice -> cmd.invoiceNumber 
    is PayInvoice -> cmd.invoiceNumber
}

// This would require adding an "id" to the BulbCommand's
fun bulbCmdToStreamId(cmd : BulbCommand) : String = "Bulb:" + when (cmd) ... 

Then you create your deciders:

val invoiceDecider = decider<InvoiceCommand, InvoiceEvent>(...)
val bulbDecider = decider<InvoiceCommand, InvoiceEvent>(...)

Then combine everything together into a new decider (this was the idea, but the syntax is just made-up):

val applicationDecider = applicationDecider(
    deciderRouting = listOf(
        DeciderRouting(::invoiceCmdToStreamId, invoiceDecider),
        DeciderRouting(::bulbCmdToStreamId, bulbDecider),
    )   
    applicationService = applicationService
)

The idea then was to just use applicationDecider as a regular decider, so that it would transparently store the events. But while I wrote this down, I can clearly see that this is not possible. There's no way "applicationDecider" can be a decider, i.e. you can't call "decide" on this thing, since you don't know the state. What you could do is callig it something else, like just "application", and then do e.g.:

val application = ...

post("/bulbs/{id}") { id, maxUses ->    
    val cmd = Fit(id, maxUses)
    application.execute(cmd)
}

post("/invoices/{invoiceNumber}") { invoiceNumber ->    
    val cmd = CreateInvoice(invoiceNumber)
    application.execute(cmd)
}

but the "application" itself wouldn't be a decider :)