Skip to content

Consolidate Entity discovery for Entity-centric and Stateful-Handler-centric - #4609

Merged
smcvb merged 39 commits into
mainfrom
bug/entity-not-found-exception
Aug 1, 2026
Merged

Consolidate Entity discovery for Entity-centric and Stateful-Handler-centric#4609
smcvb merged 39 commits into
mainfrom
bug/entity-not-found-exception

Conversation

@smcvb

@smcvb smcvb commented May 22, 2026

Copy link
Copy Markdown
Contributor

First and foremost, this pull request introduces a new test suite in the axon-integrationtest module.
The test includes 9 different entity+command handling+creation approaches:

  1. Entity-centric, no-arg entity creator
  2. Entity-centric, id-based entity creator
  3. Entity-centric, first-event-based entity creator
  4. Stateful-command-handler-centric, non-nullable entity injection, no-arg entity creator
  5. Stateful-command-handler-centric, nullable entity injection, no-arg entity creator
  6. Stateful-command-handler-centric, non-nullable entity injection, id-based entity creator
  7. Stateful-command-handler-centric, nullable entity injection,id-based entity creator
  8. Stateful-command-handler-centric, non-nullable entity injection, first-event-based entity creator
  9. Stateful-command-handler-centric, nullable entity injection,first-event-based entity creator

For all 9 styles, we have two tests:

  1. Dispatch creational command for the entity - succeeds for entity-centric and nullable entity stateful-command-handler-centric. Fails for non-null entity stateful-command-handler-centric since non-null is expected.
  2. Dispatch instance command for the entity - fails for entity-centric, fails for non-null entity stateful-command-handler-centric, succeeds for nullable entity stateful-command-handler-centric.

To ensure the above is true, I made some minor changes, which are:

  1. Introduce the EntityNotFoundException, akin to the AggregateNotFoundException. This can be thrown when applicable.
  2. Adjust the AnnotationBasedEventSourcedEntityFactory to throw an EntityNotFoundException when there is no initial event, regardless of the entity-creator choice.
  3. Adjust the InjectEntityParameterResolver to catch the EntityNotFoundException to return null for the entity when it couldn't be found.
  4. Adjust the InjectEntityParameterResolver to check for the @Nullable annotation, as well as document this behavior.

@smcvb smcvb added this to the Release 5.1.2 milestone May 22, 2026
@smcvb smcvb self-assigned this May 22, 2026
@smcvb
smcvb requested a review from a team as a code owner May 22, 2026 14:34
@smcvb
smcvb requested review from hjohn and jangalinski and removed request for a team May 22, 2026 14:34
@smcvb smcvb added the Priority 2: Should High priority. Ideally, these issues are part of the release they’re assigned to. label May 22, 2026
@smcvb smcvb added the Status: Under Discussion Use to signal that the issue in question is being discussed. label May 22, 2026
@laura-devriendt-lemon

laura-devriendt-lemon commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

My read on this issue

What I think works really well

Entity-centric instance commands throwing EntityNotFoundException when the entity was never created feels exactly right. You can't run a method "on" something that doesn't exist yet, and it stays consistent across all three creator styles.

The part I'm trying to understand

For stateful (@InjectEntity) handlers, the same "command before the entity exists" situation behaves differently depending on the creator style:

Creator style Injected entity when not yet created
no-arg non-null, empty
id-based non-null, id already filled in
event-based null

The no-arg and id-based handlers get a non-null entity even though the entity was never created, because those constructors don't need an event, so the framework can build one. In plain Java that is logical, but from a user's point of view it feels surprising: you ask for an entity that was never created and get back a real-looking instance instead of "nothing".

The id-based case is the one I find most confusing. Because the id is already set, I couldn't find a reliable way for the handler to tell that the entity was never created

The docs also present the entity-centric and stateful styles as basically interchangeable, so I wasn't sure users would expect them to diverge here.

A direction I'd suggest

Treat "no events yet means the entity doesn't exist" the same way for every creator style and every handler style:

  • @InjectEntity GiftCard card (the normal case): if it doesn't exist, the framework reports EntityNotFoundException for you, so you can't accidentally act on a blank one.
  • @InjectEntity @Nullable GiftCard card: you get null when it doesn't exist, so you can do create-or-update yourself.

That gives one existence rule everywhere, independent of which constructor the entity uses.

Pros and cons of that approach

Pros Cons
One rule everywhere. The "does it exist?" check is identical for all creator styles and both handler styles. It is a behavior change on released code (5.1 is GA), so it would target 5.2 and needs a migration note.
Removes the case where a command on a never-created entity silently succeeds. Handlers that today expect a non-null empty entity need a one-line tweak: mark the parameter @Nullable and check entity == null instead of a field.
The common case needs no null check and can't go wrong, because the framework guards it. It needs new framework support (reading @Nullable on the parameter).
"May not exist" becomes explicit in the handler signature via @Nullable. It is a bigger change than this PR, since it touches how the entity is loaded, so there is more to review.

There may well be reasons this direction is not feasible that I'm not aware of, and I realize it would be a larger redesign that needs more analysis. I mainly wanted to share my initial thoughts early and hear how you see it.

laura-devriendt-lemon

This comment was marked as outdated.

@smcvb smcvb modified the milestones: Release 5.1.2, Release 5.2.0 Jun 22, 2026
@smcvb smcvb modified the milestones: Release 5.2.0, Release 5.2.1 Jul 6, 2026
smcvb added 4 commits July 22, 2026 14:25
Add integration tests validating all entity-creator options for creational and instance command handler behavior. This means there are 12 tests against a sample domain (GiftCard), each using their unique domain implementation for testing purposes. As such there are 6 integrations of the GiftCard, being no-arg, id-based, and first-event-based entity creators, for (1) entity-centric and (2) stateful-command-handler-based command model handling
Introduce the EntityNotFoundException. This exception should be thrown whenever we are unable to find an entity. This essentially reflects the old AggregateNotFoundException from past framework installations. It should be thrown whenever a command handler cannot discover the entity to handle the command on correctly.
Adjust annotation-based EntityFactory to throw EntityNotFoundExceptions. By doing so, we ensure that the no-arg, id-based, and event-based @EntityCreator annotated field invocations fail with an EntityNotFoundExceptions when no previous event was found. Or differently put, if the entity's event stream didn't exist yet,
Catch EntityNotFoundException to return null. By doing so, an @InjectEntity annotated parameter may return null, giving the responsibility to the user to validate if the entity already exists for a given command handler.
@smcvb
smcvb force-pushed the bug/entity-not-found-exception branch from 26dfca5 to 0ccd516 Compare July 22, 2026 12:54
@smcvb
smcvb changed the base branch from axon-5.1.x to main July 22, 2026 12:54
smcvb added 4 commits July 23, 2026 17:17
Actually pass the firstEventMessage. That way, we are certain we are matching against the right parameter

#4609
Check for nullability on @InjectEntity annotated parameters. This allows a user to define a nullable entity, to clarify the validation flow in the stateful command handler better

#4609
Expand test suite to check for nullability provided for the @InjectEntity annotated parameters

#4609
Provide documentation to explain nullability of the @InjectEntity annotated parameter

#4609
@smcvb smcvb added Type: Enhancement Use to signal an issue enhances an already existing feature of the project. and removed Status: Under Discussion Use to signal that the issue in question is being discussed. labels Jul 23, 2026
@smcvb smcvb modified the milestones: Release 5.2.1, Release 5.3.0 Jul 23, 2026
@smcvb smcvb changed the title EntityNotFoundException behavior around @EntityCreator usage for Entity-centric and Stateful-Handler-based instance command handling Consolidate Entity discovery for Entity-centric and Stateful-Handler-centric Jul 24, 2026

@MateuszNaKodach MateuszNaKodach left a comment

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.

Good job, but I (and my friend Klaudiusz) had a few finidings :)

…notation/reflection/AnnotationBasedEventSourcedEntityFactory.java

Co-authored-by: Mateusz Nowak <mateusz@nakodach.pl>
smcvb added 13 commits July 31, 2026 17:43
Clarify ManagedEntity also supports @nullable

#4609
Use @nullable ManagedEntity<?, ?> to show it works

#4609
Perform EntityNotFoundException validation first to not waste resources on checks that aren't needed otherwise

#4609
Expand AnnotationUtils#hasAnnotationNamed to check the Parameter as well. This covers for the scenario where some (nullable) annotations are declarative only.

#4609
Ensure the futures are checked within a given time frame. If we don't there's a chance we'll lose a failure or success

#4609
…sted

Ensure static factory methods for @EntityCreator are supported and tested. Only do this for identifier-factory creation to limit size.

#4609
Mark EntityNotFoundException as non-transient. This was the case for the old Aggregate-based exception as well and aligns for this one too, so should be in place

#4609
Use AxonConfigurationException consistently

#4609
…ayload

Only react on null event message i.o. ScannedEntityCreator#isWithoutPayload. Doing the latter may cause incorrect exceptions to user when they, for example, simply have a typo in the payload type.

#4609
Add additional test coverage

#4609
Add comment to explain why throw cannot be reached

#4609
Catch EntityNotFoundException during load. This is required, as an EntityNotFoundException **may** mean that the user is actually waiting for a null or Optional (ManagedEntity) state object. If we don't do this, the EventSourcingRepository will not construct a ManagedEntity, which will thus not be updated for subsequent updates (read: appended events) performed by the stateful command handler using the @InjectEntity annotation in combination with @Nullable/Optional

#4609
…exception

# Conflicts:
#	examples/university-multi-tenancy-examples/university-multi-tenancy-core/src/main/java/org/axonframework/examples/demo/multitenancy/university/write/enrollstudent/EnrollStudentCommandHandler.java
@smcvb
smcvb requested a review from MateuszNaKodach July 31, 2026 16:17
Fix broken IT due to null support introduction

#4609

@hatzlj hatzlj left a comment

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.

I like the addition to support Optional and @Nullable, that makes a very clear API for handler methods. Only some nits and one comment/idea on the EventSourcedEntityFactory.create contract

@MateuszNaKodach MateuszNaKodach left a comment

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.

Good job! :) I left one minor thing and replied your comments. I believe you will introduce the change with the exceptions :)


Without either attribute set, Axon falls back to the default resolver, which reads the field marked with `@TargetEntityId` on the command payload (the form used in the example above).

[#_handling_a_missing_entity]

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.

No, I agree - I believe it's a must, but we need to make it clear :)

* @author Steven van Beelen
* @since 0.4.0
*/
public class EntityNotFoundException extends RuntimeException {

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.

Yes, I'd keep just EntityNotFoundException.

smcvb added 4 commits August 1, 2026 11:25
Emphasize any to clarify any @nullable is supported

#4609
Enforce EventSourcedEntityFactory to return null for null event. This clarifies the contract usage down stream, allowing us to having a single point where the EntityNotFoundException is thrown, namely the InitializingEntityEvolver. This cleans the AnnotationBasedEventSourcedEntityFactory up by not having mixed concerns of sometimes returning an entity and sometimes throwing. Now it just returns something or nothing.

#4609
Style and JavaDoc boyscouting

#4609
Add test validating load-then-loadOrCreate correctly uses cached entity

#4609
@smcvb
smcvb enabled auto-merge August 1, 2026 10:09
@smcvb
smcvb merged commit 28ab537 into main Aug 1, 2026
8 of 9 checks passed
@smcvb
smcvb deleted the bug/entity-not-found-exception branch August 1, 2026 10:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Priority 2: Should High priority. Ideally, these issues are part of the release they’re assigned to. Type: Enhancement Use to signal an issue enhances an already existing feature of the project.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants