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
feat: return nil aggregate state when the aggregate does not exist #505
base: master
Are you sure you want to change the base?
Conversation
I agree. It's likely to be more performant, too (if only slightly). That said, I think this wouldn't be backwards-incompatible. Anyone that previously detects a missing aggregate correctly will have their code immediately start seeing |
The intent here matters a lot, and what do you mean by "the aggregate does not exist" |
If an aggregate cannot be reconstructed from events, can we assume that it does not exist? |
I understand it to mean: "Asking for an aggregate with an ID that doesn't correspond to any known aggregate." The intuition here is that returning effectively an empty struct makes it appear that there's an aggregate where there is not really one. Returning The alternative is that detecting a bad ID is kind of non-intuitive and, if you don't check carefully, you may also start using what is effectively an uninitialized aggregate unknowingly. |
They are situations where you do not set the aggregate ID ever in the aggregate itself because you are leveraging the stream as a consistency boundary because they are many ways to "initiate" that stream to the point that a "not found" would never happen, neither "uninitialized" ... You must be extremely careful with the assumption of Aggregate has its own "Initiate State" that people must respect, and in some cases, that "initiate/default struct state" In other implementations that don't have the behavior of Elixir Structs with default values, people would have an In other words, I'm afraid I have to disagree with the assumption made here. The aggregate default/initial state must be respected, and it is not about having an event version. defmodule MyAggregate do
defstruct [status: :PENDING]
def execute(aggregate, command) do
# does not matter if the event count is 0 or not; the value, therefore, the state of the
# aggregate must be `%MyAggregate{status: :PENDING}` NOT `nil`, including from outside
# the command handler
%MyEvent{status: aggregate.status}
end
end A second argument could be about the "Initial State function," today's Elixir |
The changes impact solely the |
As I said in the comment inside the code snippet (I should move outside), regardless if you are speaking about the Command Handler context (inside the Because if you use The changes make assumptions that Whatever situation made prompted you to ask for such functionality probably could change and add enough "Domain level" information that would allow you to know better the state of your aggregate. You probably have a really good situation on hand, I am extremely curious to know what prompted you to do these changes. Do you mind sharing here? |
We can construct the initial state via
You said maybe we need a function to construct the initial state. I think it depends. For now the initial state is determined at compile time, after adding a construct function, the initial state may be determined at runtime. I don’t think it’s a good idea that the construct function generates different initial states at, because this may make the initial state undetermined, and may impact the eventual consistency, may make commands handlers more complicated. |
I shared my concern about these changes and I am strongly against them, to begin with, it would be a breaking change since such
That is exactly what I do not want to be doing in production! Such technicality should be abstracted away, reasons why some peeps (like me) use We are going in a circle by now, I asked you to share your concrete use case rather than bikeshed over code for the sake of code, everything makes sense out of context. I am not saying you are wrong completely, just that these changes are missing out on a lot of contexts and making assumptions. In the end, It Depends ™️ so I don't think it will be a productive conversation. |
@yordis Let's separate things apart, I found that problems always seem to be discussed together. 1. Why is it necessary to differentiate the initial state using the Determining whether a state(it is not the I’ll quota your code to explain. defmodule MyAggregate do
defstruct [status: :PENDING]
def execute(aggregate, command) do
%MyEvent{status: aggregate.status}
end
end The aggregate state has no obvious characteristics to distinguish whether it is in its initial state. Even if we read source code files, We can’t determinate whether the aggregate state (eg: Maybe, you could add some id-ish(please tell We can’t get the gen_server state via api, however, I do think it’s not a good idea to expose that out directly(We can discuss in other parts, not in this part). So base these, a generic way to distinguish whether an aggregate state is in its initial state is necessary. 2. How we make a compatible change If you agree with the above part, we can discuss this part further. |
What do you intend to do in your business? Like, remove the technicality of the situation for a moment. That is what I am trying to get out of you to comprehend and help you better altogether. I understand your problem with the existing implementation and the Why do you need to determine whether the aggregate state is the initial state? ESPECIALLY if you are not building dev tools and it is a domain need (concerning if so), more often than not, people think of the problem in a sub-optimal way. |
Here is my situation: FYI: Why this feature introduced, and there is a case that shares with mine, see #336 |
@fahchen For what it's worth, this is why I tend to have my enums default to an |
@yordis Maybe I'm just misunderstanding the use-case. How exactly is the |
@jvantuyl I agree with you that we can have an unknown status as the initial flag. However, if we could identify the initial state, even without altering any bytes of our business code, that would be a just enough approach. |
I would support this. Semantically, |
I think a lot of the discussion so far is a little bit unclear because of the focus on aggregate_state/3. I think @yordis (and me while I was first reading) missed that we were even talking about aggregate_state/3. There is a focus here on aggregate_state/3 and I think @yordis is right to question what you are trying to do and why aggregate_state/3 seems to be the preferred tool. To me, it's not something I would reach for unless I had run out of options elsewhere. See @jwilger's comment in #336. You've described your use case and I'm not clear why an event handler/read model wouldn't suit your needs. It would be more idiomatic IMO and you could customize to your heart's content. Most of all you'd be crafting a domain specific solution with existing mechanics instead of trying to bend the mechanics of aggregate_state/3 to your domain problem. Are you sure you can't handle this with a read model? If you elaborate more on your use case, I'm sure someone here (or in the commanded slack channel) can help find you such a solution. As for the breaking change, I don't see how aggregate_state/3 changing it's signature would be a good thing. A new function gets around that but makes the choice of when to use one over the other unclear. Finally, while nil/null is widely used in the industry to indicate the absence of a value, it is considered a anti-pattern by many. I would rather see an ok-error tuple if we have to have a change here. For fun, see null's "creator" regretting its introduction in 1965: |
I think it is more appropriate to return
nil
aggregate state when the aggregate does not exist. We can also build the initial aggregate state withstruct(aggregate_module)
easily.