Skip to content

[#4362] Allow for commands without an Entity to succeed#4388

Open
smcvb wants to merge 15 commits into
axon-5.1.xfrom
bug/4362
Open

[#4362] Allow for commands without an Entity to succeed#4388
smcvb wants to merge 15 commits into
axon-5.1.xfrom
bug/4362

Conversation

@smcvb
Copy link
Copy Markdown
Contributor

@smcvb smcvb commented Apr 3, 2026

This pull request aims to resolve #4362, which is done in two steps.

Step 1 - Ensure the EntityCommandHandlingComponent deals with EntityIdResolutionException

The original issue stems from the EntityIdResolutionException thrown by the automatically set EntityIdResolver that looks for the @TargetEntityId annoted field/value.
However, commands creating an entity are not inclined to hold this identifier at all. An entity could very well invoke a service to create the identifier for the entity in question.
Furthermore, the predicament triggered for a static command handler INSIDE an entity. When the static command handler is placed in a regular component, the issue did not trigger (because the EntityIdResolver wasn't invoked.

The combat this scenario, the EntityCommandHandlingComponent now has a try-catch around the EntityIdResolver invocation.
When an EntityIdResolutionException is thrown, the EntityCommandHandlingComponent will "guess" that the command in question is a creational command handler.
As such, it invokes the EntityMetamodel#handleCreate right away.

When the EntityMetamodel invocation fails with a NoHandlerForCommandException, we can be certain we were not dealing with a creational command handler.
Hence, we fallback to throw the original EntityIdResolutionException from the EntityCommandHandlingComponent.
However, when no NoHandlerForCommandException, the guess was correct.

Step 2 - Make an AppendCondition when none is present on EventStoreTransaction#appendEvent

Providing the fix in step 1 clarified another predicament.
There currently is no means to consciously set the AppendCondition when appending events.
Axon Framework resolves this by constructing an AppendCondition based on an EventStoreTransaction#source invocation.

This works fine, if we have something to source.
In step 1, we effectively remove any sourcing invocation by forcefully taking the EntityMetamodel#handleCreate route.
Hence, we need to have a fallback for this scenario.

Luckily, the DefaultEventStoreTransaction invokes the event-tagger lambda to construct TaggedEventMessage instances out of the given EventMessage to EventStoreTransaction#appendEvent.
If the TaggedEventMessage contains Tags, we can thus assume the user intends to append events that belong to a certain consistency boundary.

A clear example are creational events that have been published as result of creational commands that do not have a @TargetEntityId (or equivalent) annotated field/value.

However, this AppendCondition defaulting should only happen whenever we're dealing with a creational command handler within an entity. Thus, whenever the EntityMetamodel#handleCreate method is invoked without loading an entity first.
To that end, the EntityMetamodel#CREATE_WITHOUT_LOAD ResourceKey has been added.

To comply to that scenario, the DefaultEventStoreTransaction checks (1) if the the EventMessage has tags, (2) if the ProcessingContext contains the EntityMetamodel#CREATE_WITHOUT_LOAD set to true, and (3) if the current ProcessingContext does not contain an AppendCondition already.
When true we can construct a origin-based AppendCondition with the given tags.
We cannot assume any other consistency marker than origin, as the only means to get a concrete consistency marker is by sourcing.

Round-up

To test the above behavior, I have adjusted one of our integration tests by changing the creation commands to no longer hold an @TargetEntityId.
Although this means we do not test a create-or-update scenario with this command, I wagered the consequence to be neglibible. Especially given the other tests we have for create-if-missing.
Furthermore, I added some indenting and annotation changes to the touched files while working on this.


By doing the above, this PR resolves #4362

smcvb added 4 commits April 3, 2026 18:04
…utionException

If the EntityCommandHandlingComponent is unable to find an entity-id when handling a command, we may be dealing with a creational command handler. Hence, we should invoke EntityMetamodel#handleCreate. If that's successful, our guess was correct. If this call returns a NoHandlerForCommandException, we were not dealing with a creational command handler at all. In that case, we should return the original EntityIdResolutionException

#4362
When the EventStoreTransaction#appendEvent is invoked and we have tags without the existence of an AppendCondition, that signals a scenario where a creational command handler is invoked to append an event without any preceding source invocation. Thus, we should be able to assume we are dealing with the first event in this case. Making an ORIGIN marker with the found tags is restrictive, but should suffice in this case.

#4362
Adjust tests to not have a resolvable identifier for creation. Although this means a shift, there's arguably more value in testing the suggested approach (to not have identifiers to load an entity when the command handler is static) then the other way around. This shift has shown the test cases to inconsistently set creational command handlers. Furthermore, the custom EntityIdResolver does not align. Lastly, the validation changes to an AppendEventsTransactionRejectedException for duplication, as the appendCondition is violated in these cases

#4362
- Resolve nullability of getting a resource
- Clarify updating a resource may mean you need to deal with null
- Fix ugly indentation as a result of non-null/nullability settings
- Clarify the ManagedEntity may not find an entity to load

#4362
@smcvb smcvb added this to the Release 5.1.0 milestone Apr 3, 2026
@smcvb smcvb self-assigned this Apr 3, 2026
@smcvb smcvb requested a review from a team as a code owner April 3, 2026 16:28
@smcvb smcvb added the Type: Bug Use to signal issues that describe a bug within the system. label Apr 3, 2026
@smcvb smcvb requested review from corradom and hatzlj and removed request for a team April 3, 2026 16:28
@smcvb smcvb added the Priority 1: Must Highest priority. A release cannot be made if this issue isn’t resolved. label Apr 3, 2026
@smcvb smcvb requested a review from MateuszNaKodach April 3, 2026 16:28
Copy link
Copy Markdown
Contributor

@hatzlj hatzlj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good to me, no blockers

@smcvb smcvb added the Status: Under Discussion Use to signal that the issue in question is being discussed. label Apr 7, 2026
@smcvb
Copy link
Copy Markdown
Contributor Author

smcvb commented Apr 7, 2026

The failing test tests something important: publishing two events during up front. Granted, it does this in our Test Fixtures. Never the less, I think we should have a test validating that a command handler that does not source an entity can also append several tagged events without issue, as this will be a common scenario.

smcvb added 2 commits May 15, 2026 16:23
# Conflicts:
#	eventsourcing/src/main/java/org/axonframework/eventsourcing/eventstore/DefaultEventStoreTransaction.java
Changes to merge with axon-5.1.x

#4362
@smcvb smcvb requested a review from a team as a code owner May 15, 2026 14:35
@smcvb smcvb requested review from hjohn and laura-devriendt-lemon and removed request for a team May 15, 2026 14:35
Fix assetion to correclty validate the exception

#4362
@smcvb smcvb removed the Status: Under Discussion Use to signal that the issue in question is being discussed. label May 15, 2026
@smcvb
Copy link
Copy Markdown
Contributor Author

smcvb commented May 15, 2026

The failing test tests something important: publishing two events during up front. Granted, it does this in our Test Fixtures. Never the less, I think we should have a test validating that a command handler that does not source an entity can also append several tagged events without issue, as this will be a common scenario.

Having retested this, I believe this assumption was incorrect (luckily). So, moving this issue to review again.

Use computeResourceIfAbsent

#4362
Add the EntityMetamodel#CREATE_WITHOUT_LOAD resource key of type boolean. This resource should be set whenever the EntityCommandHandlingComponent decided to invoked EntityMetamodel#handleCreate **without** trying to load an entity first. This is important for the DefaultEventStoreTransaction to derive whether it should default the AppendCondition. This fine-tunes the support of the DefaultEventStoreTransaction that defaulted the AppendCondition for any set of tags, to only do this whenever a creational-command-handler is triggered.

#4362
@smcvb smcvb requested a review from hatzlj May 18, 2026 09:35
…Condition is defaulted

Set EntityMetamodel#CREATE_WITHOUT_LOAD in test validating the AppendCondition is defaulted

#4362
Copy link
Copy Markdown
Contributor

@hatzlj hatzlj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor javadoc findings, and two questions, looks good otherwise.

Comment thread modelling/src/main/java/org/axonframework/modelling/entity/EntityMetamodel.java Outdated
Copy link
Copy Markdown
Contributor

@hjohn hjohn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should solve this differently, and more cleanly. As it is, we now have an EntityMetaModel import in the eventsourcing layer. The processing context is now being used to trigger an alternative path from several layers up deep in the EventStore machinery. I think there is a real need to also offer this for more direct use cases, and by doing so we could also solve this case.

How about this:

We add/extend EventStoreTransaction API to express that we wish to create a new stream (and so no prior events should exist), either:

  • Add a flag to appendEvent
  • Add a new method appendCreationEvent

This new path then unions tags (via updateResource/orCriteria) at ORIGIN (if used more directly, users could call #appendEvent multiple times in a single transaction to create multiple streams, and that would work then as well).

That's the new API.

Then internally, we do one of these:

  • We keep the processing context flag, but resolve it MUCH earlier (still in the same layer) and use it to choose between the two appendEvent flavors
  • Or: we put a different EventAppender (decorator?) in the ProcessingContext that forwards calls to appendCreationEvent -- I think since it is created lazily, we could put it in for this case before it is otherwise created...

@smcvb
Copy link
Copy Markdown
Contributor Author

smcvb commented May 18, 2026

I think we should solve this differently, and more cleanly. As it is, we now have an EntityMetaModel import in the eventsourcing layer. The processing context is now being used to trigger an alternative path from several layers up deep in the EventStore machinery. I think there is a real need to also offer this for more direct use cases, and by doing so we could also solve this case.

How about this:

We add/extend EventStoreTransaction API to express that we wish to create a new stream (and so no prior events should exist), either:

  • Add a flag to appendEvent
  • Add a new method appendCreationEvent

This new path then unions tags (via updateResource/orCriteria) at ORIGIN (if used more directly, users could call #appendEvent multiple times in a single transaction to create multiple streams, and that would work then as well).

That's the new API.

Then internally, we do one of these:

  • We keep the processing context flag, but resolve it MUCH earlier (still in the same layer) and use it to choose between the two appendEvent flavors
  • Or: we put a different EventAppender (decorator?) in the ProcessingContext that forwards calls to appendCreationEvent -- I think since it is created lazily, we could put it in for this case before it is otherwise created...

I fully agree with the hack-part; I wasn't overly pleased with my solution either, but couldn't figure out another path.

However, both your suggestions won't fly either. Not in their current form, at least.
The EventAppender is essentially a layer around the EventSink. So, no knowledge of the EventStore or EventStoreTransaction.
And that's deliberate, as we'd otherwise enforce event publication to an EventStore, which should be a choice instead of a rule.

So, working with that limitation, the switch between EventStoreTransaction#appendEvent or EventStoreTransaction#appendEvent comes in...the EventStore.
Hence, the hacky-jump from axon-modelling to axon-eventsourcing would already have been made by than, right?
Or, is your view on hacky-ness specifically the internal detail in the DefaultEventStoreTransaction to check for this new ResourceKey?

smcvb and others added 2 commits May 18, 2026 16:19
Co-authored-by: Jakob Hatzl <hatzlj@users.noreply.github.com>
…entstore/DefaultEventStoreTransaction.java

Co-authored-by: Jakob Hatzl <hatzlj@users.noreply.github.com>
smcvb added 2 commits May 18, 2026 16:47
Drop redundant first() call

#4362
Remove resource when we're failing

#4362
Comment thread modelling/src/main/java/org/axonframework/modelling/entity/EntityMetamodel.java Outdated
@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

eventQueue.add(taggedEvent);
Set<Tag> tags = taggedEvent.tags();
if (appendingWithoutSourcing() && !tags.isEmpty()) {
// No AppendCondition is present, but the event contains tags.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait… what if you want to append unconditionally (like you append MondeyDeposited to account - you don't have max limit on account, you don't need to source anything - you just always allow to do that)?

I don’t understand why we always need an AppendCondition with tags.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's exactly what I tried to explain in the description 🙃
Let me rehash it here:

Step 2 - Make an AppendCondition when none is present on EventStoreTransaction#appendEvent

Providing the fix in step 1 clarified another predicament.
There currently is no means to consciously set the AppendCondition when appending events from within an Event-Sourced Entity (e.g. the aggregate-centric approach).
Axon Framework resolves this by constructing an AppendCondition based on an EventStoreTransaction#source invocation.

This works fine, if we have something to source.
In step 1, we effectively remove any sourcing invocation by forcefully taking the EntityMetamodel#handleCreate route.
Hence, we need to have a fallback for this scenario.

Luckily, the DefaultEventStoreTransaction invokes the event-tagger lambda to construct TaggedEventMessage instances out of the given EventMessage to EventStoreTransaction#appendEvent.
If the TaggedEventMessage contains Tags, we can thus assume the user intends to append events that belong to a certain consistency boundary.

A clear example are creational events that have been published as result of creational commands that do not have a @TargetEntityId (or equivalent) annotated field/value.

However, this AppendCondition defaulting should only happen whenever we're dealing with a creational command handler within an entity. Thus, whenever the EntityMetamodel#handleCreate method is invoked without loading an entity first.
To that end, the EntityMetamodel#CREATE_WITHOUT_LOAD ResourceKey has been added.

To comply to that scenario, the DefaultEventStoreTransaction checks (1) if the the EventMessage has tags, (2) if the ProcessingContext contains the EntityMetamodel#CREATE_WITHOUT_LOAD set to true, and (3) if the current ProcessingContext does not contain an AppendCondition already.
When true we can construct a origin-based AppendCondition with the given tags.
We cannot assume any other consistency marker than origin, as the only means to get a concrete consistency marker is by sourcing.

@smcvb smcvb requested review from MateuszNaKodach and hjohn May 19, 2026 07:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Priority 1: Must Highest priority. A release cannot be made if this issue isn’t resolved. Type: Bug Use to signal issues that describe a bug within the system.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants