Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DecideFunction - why Vec<E> #7

Closed
ms-ati opened this issue Sep 29, 2023 · 2 comments
Closed

DecideFunction - why Vec<E> #7

ms-ati opened this issue Sep 29, 2023 · 2 comments

Comments

@ms-ati
Copy link

ms-ati commented Sep 29, 2023

Thanks so much for pushing this work forward as a community project ❤️ 👏🏻 🎉

As a practitioner of Event Sourcing based on Greg Young's training materials across a couple companies and three languages now, I have some in-the-weeds questions if you are open to discussing them here? If not, that's fine, and thanks!

1. Why does DecideFunction return Vec<E>?

Yes, it totally makes sense to return Events rather than imperatively store them

In a traditional CommandHandler as taught by Greg Young we often see in C# an interface like this:

public interface ICommandHandler<TCommand> where TCommand : Message {
    void Handle(TCommand message);
}

👍🏻 I'm 100% understanding (and agreeing!) that a design goal of Fmodel appears to be bringing some power of functional programming to these patterns, so of course we want to return the Events.

By contrast the traditional Greg Young-provided code patterns as above are deeply imperative, and as we see in official examples, the void Handle(TCommand message) implies the following dependency management chain being called as side effects:

  1. ICommandHandler#Handle has an IRepository (see DDD Repository pattern)
  2. IRepository#Save(AggregateRoot aggregate, int expectedVersion) if you are doing Event Sourcing for this aggregate has an IEventStore
  3. IEventStore#SaveEvents(Guid aggregateId, IEnumerable<Event> events, int expectedVersion) has a concrete implementation of recording the events to the stream of aggregateId or returning an Optimistic Concurrency error if the expectedVersion is behind the last event saved in the stream

So it makes sense to return the Event(s), but why a Vec?

Yes, it totally makes sense that sometimes a Command generates 2+ domain Events

Sometimes the best way to model the domain is for one Command to result in multiple Events (e.g. an Axon framework example, an Elixer example, a C# example).

Although I vaguely recall that in one of the training videos, Greg Young suggests that we should always question instances of 1:N Command:Events pattern when it comes up -- I vaguely recall the suggestion that 1:1 pairing between Command and Event are generally best at modeling that "a user intent succeeded and it is now a fact", and so this should be the common case?

So it make sense there could be 0, 1, or more Events, but why a Vec?

Do we really need to allocate an owning Vec on every single Command that succeeds?

My understanding is that by making the return type of DecideFunction Vec<E>, we are saying a side-effect of every single decision about a Command in the system will be a heap allocation. Is this the right approach? I'm assuming the following:

  1. Most decisions on a Command should return either a rejection (more on this) or 1 Event
  2. A few could result in 2-3 Events, and returning more than 3 would be quite rare
  3. The resulting events in memory are likely to be short-lived and handled quickly - recorded to a durable event stream and perhaps applied to a model or published to a notification system

If those assumptions are correct, I was wondering if the return type should be more like:

-> Result<CommandFailedError, impl Iterator<Item=E> + '_ >

In other words, an explicitly modeled rejection of the command, or an interface that allows a zero cost implementation such as a slice of 1-2 Events on the stack?

Q1: Would such an approach allow returning a slice containing a single Event on the stack, avoiding allocation but preserving that the return is iterable?

Q2: Why does Fmodel not model Rejection of Commands in a first class way, such via a Result type?

Thank you!

@idugalic
Copy link
Member

Hi, thanks for your question!

Decider belongs to the pure domain layer. It is not doing the fetching or storing events or any other side effect on this level.
I hope this blog post will help: https://fraktalio.com/blog/side-effects-storing-and-fetching-the-data.html

Decider is already returning a list/vector of events. It is valuable to model error events, and in the case of a business error you can publish an error event instead of a Result or Panic. Please notice how these error events are business errors, not infra/technical errors. Events are modeled with Enum, so you can do pattern matching on them, same as Result!

I agree that one command should produce only one event. But there are exceptions to that rule sometimes. The Vector is more abstract and it covers all the cases.

I hope this blog post will help: https://fraktalio.com/blog/unmanaged-hazards-exceptions.html

Once you have your Decider in place you are ready to run it. Application layer components will use that Decider to compute and repositories to fetch/store the data. EventSourcedAggregate is that component. It is using/composing the aggregate API that you can use from your adapters: Web.

// Handles the command by fetching the events from the repository, computing new events based on the current events and the command, and saving the new events to the repository.
    pub async fn handle(&self, command: &C) -> Result<Vec<(E, Version)>, Error> {
        let events: Vec<(E, Version)> = self.repository.fetch_events(command).await?;
        let mut version: Option<Version> = None;
        let mut current_events: Vec<E> = vec![];
        for (event, ver) in events {
            version = Some(ver);
            current_events.push(event);
        }
        let new_events = self.decider.compute_new_events(&current_events, command);
        let saved_events = self.repository.save(&new_events, &version).await?;
        Ok(saved_events)
    }

On this level (application level) you have a handle method that returns the Result!
The question is if we should publish that list of just stored events in it. We can choose to go with simply success/error.

@idugalic
Copy link
Member

We are also using Box to box the functions. This is also heap allocation. But, it is much easier to manage and control. I am happy to pay that price.

@idugalic idugalic closed this as completed Dec 9, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants