Repository: AxonFramework/AxonFramework
Pull request #3353: [#3287] Annotated @EventSourcedEntity support
Author: login: CodeDrivenMitch
Labels: name: Type: Feature
description: Use to signal an issue is completely new to the project.
name: Priority 1: Must
description: Highest priority. A release cannot be made if this issue isn’t resolved.
Created: 2025-03-27T12:36:48Z | Merged: 2025-03-31T16:42:30Z

This PR introduces the `AnnotationBasedEventSourcingEntityRepository` and in doing so, resolves #3287.

The `AnnotationBasedEventSourcingEntityRepository` is a wrapper of the `AsyncEventSourcingRepository` that takes in the id class, entity class,and the backing `EventStore`. The user is able to configure this repository through properties on the `@EventSourcedEntity` annotation. 

There are two very important things to configure: The resolving of `EventCriteria`, and the ability to construct an empty instance of the entity to play the events on. 

## `EventCriteria` resolving

The user has 4 options to define the `EventCriteria`. In order of precedence:

1. Define a custom `EventCriteriaResolver` class on the annotation
2. Static methods in the entity annotated with `@EventCriteriaResolver`
3. The `tagKey` property of the annotation
4. The simple name of the class will be used

This behaviour is extensively documented in the `EventSourcedEntity`.

An example:

```java
@EventSourcedEntity(tagKey = "fallbackTagKey") // Used if no matches for builder methods
class FunctionalEventSourcedEntity {

    @EventCriteriaBuilder
    public static EventCriteria buildCriteria(String id) {
        return EventCriteria.match().eventsOfAnyType().withTags("aggregateIdentifierOfString", id);
    }

    @EventCriteriaBuilder
    public static EventCriteria buildCriteria(Long id) {
        return EventCriteria.match().eventsOfAnyType().withTags("aggregateIdentifierOfLong", id.toString());
    }

}
```

## Creation of entity
While unlikely that the user wants more than a 0 arg constructor, or a constructor with the id as argument, the user can customize this default behaviour on the annotation.

So, the following entities are valid without any customization:

```java
class MyEntity {}

class MySecondEntity {
    public MySecondEntity() {}
}

class MyThirdEntity {
    public MyThirdEntity(String id) {}
}
```
== Notes
- I have chosen to make the `EventStateApplier` non-configurable, and always construct an `AnnotationBasedEventStateApplier`. This is up for discussion, as it's easy to add to the annotation as well.
- a new util, `ConstructorUtils` has been introduced to look up constructors and create functions that can be cached

Linked issue #3287: `CriteriaResoler` for `SourcingCondition` from ID
### Feature Description

The `AsyncEventSourcingRepository` uses a `CriteriaResolver` to resolve the `EventCriteria` object used for the `SourcingCondition`.
The `SourcingCondition`, in turn, is used to source a (command) model from the `EventStore`.

The `CriteriaResolver` uses a parameter of generic type `ID`, which is specific for a given `AsyncEventSourcingRepository`.
Thus, a `CriteriaResolver` should be defined based on the `Tags`/identifiers present on the (command) model supported by the `AsyncEventSourcingRepository`.

### Current Behaviour

There is no dedicated `CriteriaResolver` implementation yet.

### Wanted Behaviour

That we have a `CriteriaResolver` implementation that can construct an `SourcingCondition` for a given generic `ID`.

### Possible Workarounds

None.

Linked issue comments: user: login: smcvb
created_at: 2025-04-01T11:35:33Z
body: I am closing this issue as resolved through pull request #3353.
author_association: CONTRIBUTOR

Diff:
diff --git a/eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/AnnotationBasedEventCriteriaResolver.java b/eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/AnnotationBasedEventCriteriaResolver.java
new file mode 100644
index 00000000000..d163580ee33
--- /dev/null
+++ b/eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/AnnotationBasedEventCriteriaResolver.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+import jakarta.annotation.Nonnull;
+import org.axonframework.common.ReflectionUtils;
+import org.axonframework.common.annotation.AnnotationUtils;
+import org.axonframework.common.infra.ComponentDescriptor;
+import org.axonframework.common.infra.DescribableComponent;
+import org.axonframework.eventsourcing.CriteriaResolver;
+import org.axonframework.eventsourcing.eventstore.EventCriteria;
+import org.axonframework.eventsourcing.eventstore.Tag;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Annotation-based {@link CriteriaResolver} implementation which resolves {@link EventCriteria} based on the given
+ * {@code id} for loading the entity. This is the default when using the {@link EventSourcedEntity} annotation.
+ * <p>
+ * There are various ways to define how the {@link EventCriteria} should be resolved. In order of precedence:
+ * <ol>
+ *     <li>
+ *         By defining a static method in the entity class annotated with {@link EventCriteriaBuilder} which returns an
+ *         {@link EventCriteria} and accepts the {@code id} as a parameter. This method should be static and return an
+ *         {@link EventCriteria}. Multiple methods can be defined with different id types, and the first matching method
+ *         will be used.
+ *     </li>
+ *     <li>
+ *         If no matching {@link EventCriteriaBuilder} is found, the {@link EventSourcedEntity#tagKey()} will be used as the tag key, and the {@link Object#toString()} of the id will be used as value.
+ *     </li>
+ *     <li>
+ *         If the {@link EventSourcedEntity#tagKey()} is empty, the {@link Class#getSimpleName()} of the entity will be used as tag key, and the {@link Object#toString()} of the id will be used as value.
+ *     </li>
+ * </ol>
+ * <p>
+ * Different methods can be combined, as can several {@link EventCriteriaBuilder} methods be defined as long as they
+ * are for different id types. This resolver is the default when using the {@link EventSourcedEntity} annotation,
+ * but specifying a custom {@link CriteriaResolver} will override this behavior.
+ *
+ *
+ * @author Mitchell Herrijgers
+ * @see EventSourcedEntity
+ * @since 5.0.0
+ */
+public class AnnotationBasedEventCriteriaResolver implements CriteriaResolver<Object>, DescribableComponent {
+
+    private final Class<?> entityType;
+    private final String tagKey;
+    private final Map<Class<?>, Method> builderMap;
+
+    /**
+     * Initialize the resolver for the given {@code entityType}. The entity type should be annotated with
+     * {@link EventSourcedEntity}, or this resolver will throw an {@link IllegalArgumentException}.
+     * <p>
+     * Will check for methods annotated with {@link EventCriteriaBuilder} and store them in a map for later use. If one
+     * of the methods is invalid as defined in the Javadoc of {@link EventCriteriaBuilder}, an
+     * {@link IllegalArgumentException} will be thrown.
+     *
+     * @param entityType The entity type to resolve criteria for.
+     */
+    public AnnotationBasedEventCriteriaResolver(@Nonnull Class<?> entityType) {
+        this.entityType = Objects.requireNonNull(entityType, "The entity type cannot be null.");
+
+
+        Map<String, Object> attributes = AnnotationUtils
+                .findAnnotationAttributes(entityType, EventSourcedEntity.class)
+                .orElseThrow(() -> new IllegalArgumentException("The given class is not an @EventSourcedEntity"));
+
+        String annotationTagKey = (String) attributes.get("tagKey");
+        this.tagKey = annotationTagKey.isEmpty() ? null : annotationTagKey;
+
+        Set<Method> eventCriteriaBuilders = Arrays.stream(entityType.getMethods())
+                                                  .filter(m -> m.isAnnotationPresent(EventCriteriaBuilder.class))
+                                                  .collect(Collectors.toSet());
+
+        // Validate each method meets requirements
+        eventCriteriaBuilders.forEach(AnnotationBasedEventCriteriaResolver::validateEventCriteriaBuilderMethod);
+
+        // Check for duplicate parameter types
+        eventCriteriaBuilders.stream()
+                             .collect(Collectors.groupingBy(m -> m.getParameterTypes()[0]))
+                             .values()
+                             .stream()
+                             .filter(list -> list.size() > 1)
+                             .findAny()
+                             .ifPresent(list -> {
+                                 throw new IllegalArgumentException(
+                                         "Multiple @EventCriteriaBuilder methods found with the same parameter type: %s".formatted(
+                                                 list.stream()
+                                                     .map(ReflectionUtils::toDiscernibleSignature)
+                                                     .sorted()
+                                                     .collect(Collectors.joining(", "))));
+                             });
+
+        this.builderMap = eventCriteriaBuilders.stream()
+                                .collect(Collectors.toMap(m -> m.getParameterTypes()[0],
+                                                          m -> m));
+    }
+
+    @Override
+    public EventCriteria apply(Object id) {
+        Optional<Object> builderResult = builderMap
+                .keySet()
+                .stream()
+                .filter(c -> c.isInstance(id))
+                .findFirst()
+                .map(builderMap::get)
+                .map(m -> {
+                    Object result;
+                    try {
+                        result = m.invoke(null, id);
+                    } catch (Exception e) {
+                        throw new IllegalArgumentException("Error invoking EventCriteriaBuilder method", e);
+                    }
+                    if (!(result instanceof EventCriteria)) {
+                        throw new IllegalArgumentException(
+                                "The @EventCriteriaBuilder method returned null. The method must return a non-null EventCriteria. Violating method: %s".formatted(
+                                        ReflectionUtils.toDiscernibleSignature(m)));
+                    }
+                    return result;
+                });
+        if (builderResult.isPresent()) {
+            return (EventCriteria) builderResult.get();
+        }
+        String key = Objects.requireNonNullElseGet(tagKey, entityType::getSimpleName);
+        return EventCriteria.match()
+                            .eventsOfAnyType()
+                            .withTags(Tag.of(key, id.toString()));
+    }
+
+    private static void validateEventCriteriaBuilderMethod(Method m) {
+        if (!EventCriteria.class.isAssignableFrom(m.getReturnType())) {
+            throw new IllegalArgumentException(
+                    "Method annotated with @EventCriteriaBuilder must return an EventCriteria. Violating method: %s".formatted(
+                            ReflectionUtils.toDiscernibleSignature(m)));
+        }
+        if (m.getParameterCount() != 1) {
+            throw new IllegalArgumentException(
+                    "Method annotated with @EventCriteriaBuilder must have exactly one parameter. Violating method: %s".formatted(
+                            ReflectionUtils.toDiscernibleSignature(m)));
+        }
+        if (!Modifier.isStatic(m.getModifiers())) {
+            throw new IllegalArgumentException(
+                    "Method annotated with @EventCriteriaBuilder must be static. Violating method: %s".formatted(
+                            ReflectionUtils.toDiscernibleSignature(m)));
+        }
+    }
+
+    @Override
+    public void describeTo(@Nonnull ComponentDescriptor descriptor) {
+        descriptor.describeProperty("entityType", entityType.getName());
+        descriptor.describeProperty("tagKey", tagKey);
+        descriptor.describeProperty("criteriaBuilders", builderMap);
+    }
+}
diff --git a/eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/AnnotationBasedEventSourcingEntityRepository.java b/eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/AnnotationBasedEventSourcingEntityRepository.java
new file mode 100644
index 00000000000..d446a893375
--- /dev/null
+++ b/eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/AnnotationBasedEventSourcingEntityRepository.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+import jakarta.annotation.Nonnull;
+import org.axonframework.common.annotation.AnnotationUtils;
+import org.axonframework.common.infra.ComponentDescriptor;
+import org.axonframework.eventsourcing.AnnotationBasedEventStateApplier;
+import org.axonframework.eventsourcing.AsyncEventSourcingRepository;
+import org.axonframework.eventsourcing.CriteriaResolver;
+import org.axonframework.eventsourcing.eventstore.AsyncEventStore;
+import org.axonframework.messaging.unitofwork.ProcessingContext;
+import org.axonframework.modelling.repository.AsyncRepository;
+import org.axonframework.modelling.repository.ManagedEntity;
+
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+import static org.axonframework.common.ConstructorUtils.factoryForTypeWithOptionalArgument;
+
+/**
+ * Implementation of a {@link AsyncRepository} that configures an entity based on the configuration provided by the
+ * {@link EventSourcedEntity} annotation. Please refer to the {@link EventSourcedEntity} annotation for more information
+ * on the configuration options.
+ *
+ * @param <ID> The type of the identifier of the entity.
+ * @param <T>  The type of the entity.
+ */
+public class AnnotationBasedEventSourcingEntityRepository<ID, T> implements AsyncRepository.LifecycleManagement<ID, T> {
+
+    private final Class<ID> idType;
+    private final Class<T> entityType;
+    private final AsyncEventSourcingRepository<ID, T> repository;
+    private final EventSourcedEntityFactory<ID, T> creator;
+    private final CriteriaResolver<ID> criteriaResolver;
+    private final AnnotationBasedEventStateApplier<T> stateApplier;
+
+    /**
+     * Initialize a repository for the given {@code entityType} and {@code idType} using the provided
+     * {@code eventStore}.
+     *
+     * @param eventStore The event store to store and load events from.
+     * @param idType     The type of the identifier of the entity.
+     * @param entityType The type of the entity.
+     */
+    @SuppressWarnings("unchecked")
+    public AnnotationBasedEventSourcingEntityRepository(
+            AsyncEventStore eventStore,
+            Class<ID> idType,
+            Class<T> entityType
+    ) {
+        Map<String, Object> annotationAttributes = AnnotationUtils
+                .findAnnotationAttributes(entityType, EventSourcedEntity.class)
+                .orElseThrow(() -> new IllegalArgumentException("The given class is not an @EventSourcingEntity"));
+
+        this.idType = idType;
+        this.entityType = entityType;
+
+        var creatorType = (Class<EventSourcedEntityFactory<ID, T>>) annotationAttributes.get("entityFactory");
+        var criteriaResolverType = (Class<CriteriaResolver<ID>>) annotationAttributes.get("criteriaResolver");
+        this.creator = factoryForTypeWithOptionalArgument(creatorType, Class.class).apply(entityType);
+        this.criteriaResolver = factoryForTypeWithOptionalArgument(criteriaResolverType, Class.class).apply(entityType);
+
+        this.stateApplier = new AnnotationBasedEventStateApplier<>(entityType);
+        this.repository = new AsyncEventSourcingRepository<>(
+                idType,
+                entityType,
+                eventStore,
+                criteriaResolver,
+                stateApplier,
+                id -> creator.createEntity(entityType, id)
+        );
+    }
+
+
+    @Nonnull
+    @Override
+    public Class<T> entityType() {
+        return entityType;
+    }
+
+    @Nonnull
+    @Override
+    public Class<ID> idType() {
+        return idType;
+    }
+
+    @Override
+    public CompletableFuture<ManagedEntity<ID, T>> load(@Nonnull ID identifier,
+                                                        @Nonnull ProcessingContext processingContext) {
+        return repository.load(identifier, processingContext);
+    }
+
+    @Override
+    public CompletableFuture<ManagedEntity<ID, T>> loadOrCreate(@Nonnull ID identifier,
+                                                                @Nonnull ProcessingContext processingContext) {
+        return repository.loadOrCreate(identifier, processingContext);
+    }
+
+    @Override
+    public ManagedEntity<ID, T> persist(@Nonnull ID identifier, @Nonnull T entity,
+                                        @Nonnull ProcessingContext processingContext) {
+        return repository.persist(identifier, entity, processingContext);
+    }
+
+    @Override
+    public void describeTo(@Nonnull ComponentDescriptor descriptor) {
+        descriptor.describeProperty("entityType", entityType);
+        descriptor.describeProperty("idType", idType);
+        descriptor.describeProperty("creator", creator);
+        descriptor.describeProperty("criteriaResolver", criteriaResolver);
+        descriptor.describeProperty("stateApplier", stateApplier);
+        descriptor.describeWrapperOf(repository);
+    }
+
+    @Override
+    public ManagedEntity<ID, T> attach(@Nonnull ManagedEntity<ID, T> entity,
+                                       @Nonnull ProcessingContext processingContext) {
+        return repository.attach(entity, processingContext);
+    }
+}
diff --git a/eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/ConstructorBasedEventSourcedEntityFactory.java b/eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/ConstructorBasedEventSourcedEntityFactory.java
new file mode 100644
index 00000000000..5a2869f8d33
--- /dev/null
+++ b/eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/ConstructorBasedEventSourcedEntityFactory.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+import jakarta.annotation.Nonnull;
+import org.axonframework.common.infra.ComponentDescriptor;
+import org.axonframework.common.infra.DescribableComponent;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+
+import static org.axonframework.common.ConstructorUtils.factoryForTypeWithOptionalArgument;
+
+/**
+ * {@link EventSourcedEntityFactory} implementation which uses a constructor to create a new instance of an entity. The
+ * constructor can either have a single argument of the same type as the identifier, or no arguments at all.
+ *
+ * @param <M> The type of the entity to create.
+ * @author Mitchell Herrijgers
+ * @since 5.0.0
+ */
+public class ConstructorBasedEventSourcedEntityFactory<M>
+        implements EventSourcedEntityFactory<Object, M>, DescribableComponent {
+
+    private final Class<M> entityType;
+    private final Map<Class<?>, Function<Object, M>> constructorCache = new ConcurrentHashMap<>();
+
+    /**
+     * Create an {@link EventSourcedEntityFactory} for the given {@code entityType} based on its constructors.
+     * @param entityType The type of entity to create.
+     */
+    public ConstructorBasedEventSourcedEntityFactory(@Nonnull Class<M> entityType) {
+        this.entityType = Objects.requireNonNull(entityType, "The entity type cannot be null.");
+    }
+
+    @Override
+    public M createEntity(@Nonnull Class<M> entityType, @Nonnull Object id) {
+        return constructorCache
+                .computeIfAbsent(id.getClass(),
+                                 (i) -> factoryForTypeWithOptionalArgument(entityType, i))
+                .apply(id);
+    }
+
+    @Override
+    public void describeTo(@Nonnull ComponentDescriptor descriptor) {
+        descriptor.describeProperty("entityType", entityType.getName());
+        descriptor.describeProperty("constructorCache", constructorCache);
+    }
+}
diff --git a/eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/EventCriteriaBuilder.java b/eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/EventCriteriaBuilder.java
new file mode 100644
index 00000000000..837f89e5c08
--- /dev/null
+++ b/eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/EventCriteriaBuilder.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+import org.axonframework.eventsourcing.eventstore.EventCriteria;
+import org.axonframework.modelling.command.annotation.TargetEntityId;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to indicate that a method can be used to resolve the {@link EventCriteria} based on the
+ * {@link TargetEntityId} when loading an {@link EventSourcedEntity}.
+ * <p>
+ * The method should be:
+ * <ul>
+ *     <li>A static method</li>
+ *     <li>Accept a single parameter of the type of the {@link TargetEntityId} of the {@link EventSourcedEntity}</li>
+ *     <li>Return an {@link EventCriteria} instance</li>
+ * </ul>
+ * <p>
+ * In all other cases, an exception will be thrown during initialization of the
+ * {@link AnnotationBasedEventSourcingEntityRepository}.
+ *
+ * @author Mitchell Herrijgers
+ * @see AnnotationBasedEventSourcingEntityRepository
+ * @see TargetEntityId
+ * @see EventSourcedEntity
+ * @since 5.0.0
+ */
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface EventCriteriaBuilder {
+
+}
diff --git a/eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/EventSourcedEntity.java b/eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/EventSourcedEntity.java
new file mode 100644
index 00000000000..622c8f9c8ed
--- /dev/null
+++ b/eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/EventSourcedEntity.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+import org.axonframework.eventsourcing.CriteriaResolver;
+import org.axonframework.eventsourcing.EventSourcingRepository;
+import org.axonframework.eventsourcing.eventstore.EventCriteria;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to configure several aspects of an event-sourced entity. This annotation is required to construct a
+ * {@link AnnotationBasedEventSourcingEntityRepository}.
+ * <p>
+ * While loading the entity from the {@link EventSourcingRepository}, the provided {@code id} needs to be translated to
+ * an {@link EventCriteria} instance to load the correct events. Unless overridden, this translation is done by the
+ * {@link AnnotationBasedEventCriteriaResolver}. So, {@link EventCriteria} will be resolved in the following order:
+ * <ol>
+ *     <li>
+ *         Using the custom {@link #criteriaResolver()}. The provided class should implement the {@link CriteriaResolver},
+ *         and have a no-arg constructor, or a 1-arg constructor with the {@link Class} of the entity as parameter.
+ *     </li>
+ *     <li>
+ *         By defining a static method in the entity class annotated with {@link EventCriteriaBuilder} which returns an
+ *         {@link EventCriteria} and accepts the {@code id} as a parameter. This method should be static and return an
+ *         {@link EventCriteria}. Multiple methods can be defined with different id types, and the first matching method
+ *         will be used.
+ *     </li>
+ *     <li>
+ *         If no matching {@link EventCriteriaBuilder} is found, the {@link EventSourcedEntity#tagKey()} will be used as the tag key, and the {@link Object#toString()} of the id will be used as value.
+ *     </li>
+ *     <li>
+ *         If the {@link EventSourcedEntity#tagKey()} is empty, the {@link Class#getSimpleName()} of the entity will be used as tag key, and the {@link Object#toString()} of the id will be used as value.
+ *         Note that the tag format rules are undecided until <a href="https://github.com/AxonFramework/AxonFramework/issues/3326">issue 3326/a> is resolved. This may change in the future.
+ *     </li>
+ * </ol>
+ *
+ * <p>
+ * The {@link #entityFactory()} is used to create a new instance of the entity. The provided class should implement the
+ * {@link EventSourcedEntityFactory} interface, and have a no-arg constructor, or a 1-arg constructor with the {@link Class} of the entity as parameter.
+ * By default, the {@link ConstructorBasedEventSourcedEntityFactory} is used, which creates a new instance using the
+ * no-arg constructor of the entity class, or a 1-arg constructor with the id as parameter.
+ *
+ * @author Mitchell Herrijgers
+ * @see EventSourcingRepository
+ * @see CriteriaResolver
+ * @see AnnotationBasedEventCriteriaResolver
+ * @see EventCriteriaBuilder
+ * @see EventSourcedEntityFactory
+ * @since 5.0.0
+ */
+@SuppressWarnings("unused")
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface EventSourcedEntity {
+
+    /**
+     * The tag name to use when resolving the {@link EventCriteria} for the entity. If empty, the simple name of the
+     * entity class will be used.
+     * <p>
+     * This value does not take effect if a matching {@link EventCriteriaBuilder} is found, or a custom
+     * {@link #criteriaResolver()} is provided.
+     *
+     * @return The tag name to use when resolving the {@link EventCriteria} for the entity.
+     */
+    String tagKey() default "";
+
+    /**
+     * The class to use to resolve the {@link EventCriteria} for the entity. The provided class should implement the
+     * {@link CriteriaResolver} interface, and have a no-arg constructor, or a 1-arg constructor with the {@link Class}
+     * of the entity as parameter.
+     * @return The class to use to resolve the {@link EventCriteria} for the entity.
+     */
+    Class<? extends CriteriaResolver> criteriaResolver() default AnnotationBasedEventCriteriaResolver.class;
+
+    /**
+     * The class to use to create a new instance of the entity. The provided class should implement the
+     * {@link EventSourcedEntityFactory} interface, and have a no-arg constructor, or a 1-arg constructor with the
+     * {@link Class} of the entity as parameter.
+     * @return The class to use to create a new instance of the entity.
+     */
+    Class<? extends EventSourcedEntityFactory> entityFactory() default ConstructorBasedEventSourcedEntityFactory.class;
+}
diff --git a/eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/EventSourcedEntityFactory.java b/eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/EventSourcedEntityFactory.java
new file mode 100644
index 00000000000..635e5559326
--- /dev/null
+++ b/eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/EventSourcedEntityFactory.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+import jakarta.annotation.Nonnull;
+
+/**
+ * Functional interface towards creating a new instance of an entity of the given {@code entityType} and the given
+ * {@code id}.
+ *
+ * @since 5.0.0
+ * @author Mitchell Herrijgers
+ * @param <ID> The type of the identifier of the entity to create.
+ * @param <E>  The type of the entity to create.
+ */
+@FunctionalInterface
+public interface EventSourcedEntityFactory<ID, E> {
+
+    /**
+     * Creates a new instance of an entity of the given {@code entityType} and {@code idType}.
+     *
+     * @param entityType The type of the entity to create.
+     * @param id         The identifier of the entity to create.
+     * @return A new instance of the entity.
+     */
+    E createEntity(@Nonnull Class<E> entityType, @Nonnull ID id);
+}
\ No newline at end of file
diff --git a/eventsourcing/src/main/java/org/axonframework/eventsourcing/eventstore/Tag.java b/eventsourcing/src/main/java/org/axonframework/eventsourcing/eventstore/Tag.java
index adb3742c043..bc02ed62f3c 100644
--- a/eventsourcing/src/main/java/org/axonframework/eventsourcing/eventstore/Tag.java
+++ b/eventsourcing/src/main/java/org/axonframework/eventsourcing/eventstore/Tag.java
@@ -23,6 +23,7 @@
 import org.axonframework.messaging.Context.ResourceKey;
 import org.axonframework.messaging.unitofwork.ProcessingContext;
 
+import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 
@@ -83,4 +84,18 @@ public static Context addToContext(Context context, Set<Tag> tags) {
     public static Optional<Set<Tag>> fromContext(Context context) {
         return Optional.ofNullable(context.getResource(RESOURCE_KEY));
     }
+
+    /**
+     * Creates a new {@code Tag} with the given {@code key} and {@code value}.
+     *
+     * @param key   The key of the new {@code Tag}.
+     * @param value The value of the new {@code Tag}.
+     * @return A new {@code Tag} with the given {@code key} and {@code value}.
+     */
+    public static Tag of(@Nonnull String key, @Nonnull String value) {
+        return new Tag(
+                Objects.requireNonNull(key, "The key of a Tag is required."),
+                Objects.requireNonNull(value, "The value of a Tag is required.")
+        );
+    }
 }
diff --git a/eventsourcing/src/test/java/org/axonframework/eventsourcing/annotation/AnnotationBasedEventCriteriaResolverTest.java b/eventsourcing/src/test/java/org/axonframework/eventsourcing/annotation/AnnotationBasedEventCriteriaResolverTest.java
new file mode 100644
index 00000000000..1ae93824d82
--- /dev/null
+++ b/eventsourcing/src/test/java/org/axonframework/eventsourcing/annotation/AnnotationBasedEventCriteriaResolverTest.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+import org.axonframework.eventsourcing.eventstore.EventCriteria;
+import org.junit.jupiter.api.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class AnnotationBasedEventCriteriaResolverTest {
+
+    @Test
+    void resolvedRightBuilderMethodForDifferentIdTypes() {
+        var resolver = new AnnotationBasedEventCriteriaResolver(FunctionalEventSourcedEntity.class);
+
+        var criteriaString = resolver.resolve("id");
+        assertEquals(EventCriteria.match().eventsOfAnyType().withTags("aggregateIdentifierOfString", "id"), criteriaString);
+
+        var criteriaLong = resolver.resolve(1L);
+        assertEquals(EventCriteria.match().eventsOfAnyType().withTags("aggregateIdentifierOfLong", "1"), criteriaLong);
+    }
+
+    @Test
+    void throwsWhenEventCriteriaBuilderReturnsNull() {
+        var resolver = new AnnotationBasedEventCriteriaResolver(FunctionalEventSourcedEntity.class);
+
+        var exception = Assertions.assertThrows(
+                IllegalArgumentException.class,
+                () -> resolver.resolve(1)
+        );
+        assertEquals(
+                "The @EventCriteriaBuilder method returned null. The method must return a non-null EventCriteria. Violating method: buildCriteria(java.lang.Integer)",
+                exception.getMessage()
+        );
+    }
+
+    @Test
+    void usesTagKeyPropertyIfNoEventCriteriaBuilderMatches() {
+        var resolver = new AnnotationBasedEventCriteriaResolver(FunctionalEventSourcedEntity.class);
+
+        var criteria = resolver.resolve(0.0);
+        assertEquals(EventCriteria.match().eventsOfAnyType().withTags("fallbackTagKey", "0.0"), criteria);
+    }
+
+    @EventSourcedEntity(tagKey = "fallbackTagKey")
+    class FunctionalEventSourcedEntity {
+
+        @EventCriteriaBuilder
+        public static EventCriteria buildCriteria(String id) {
+            return EventCriteria.match().eventsOfAnyType().withTags("aggregateIdentifierOfString", id);
+        }
+
+        @EventCriteriaBuilder
+        public static EventCriteria buildCriteria(Long id) {
+            return EventCriteria.match().eventsOfAnyType().withTags("aggregateIdentifierOfLong", id.toString());
+        }
+
+        @EventCriteriaBuilder
+        public static EventCriteria buildCriteria(Integer id) {
+            return null;
+        }
+    }
+
+    @Test
+    void usesSimpleClassNameAsTagNameIfNoTagKeyPropertyIsSet() {
+        var resolver = new AnnotationBasedEventCriteriaResolver(DefaultEventSourcedEntity.class);
+
+        var criteria = resolver.resolve("id");
+        assertEquals(EventCriteria.match().eventsOfAnyType().withTags("DefaultEventSourcedEntity", "id"), criteria);
+    }
+
+    @EventSourcedEntity()
+    class DefaultEventSourcedEntity {
+    }
+
+    @Nested
+    class ConfigurationProblems {
+
+        @Test
+        void testEventCriteriaBuilderWithNonStaticBuilderThrowsException() {
+            var exception = Assertions.assertThrows(
+                    IllegalArgumentException.class,
+                    () -> new AnnotationBasedEventCriteriaResolver(EntityWithNonStaticEventCriteriaBuilder.class));
+            assertEquals(
+                    "Method annotated with @EventCriteriaBuilder must be static. Violating method: buildCriteria(java.lang.String)",
+                    exception.getMessage()
+            );
+        }
+
+        @EventSourcedEntity
+        class EntityWithNonStaticEventCriteriaBuilder {
+
+            @EventCriteriaBuilder
+            public EventCriteria buildCriteria(String id) {
+                return EventCriteria.match().eventsOfAnyType().withTags("aggregateIdentifier", id);
+            }
+        }
+
+        @Test
+        void testEventCriteriaBuilderWithZeroArgBuilderThrowsException() {
+            var exception = Assertions.assertThrows(
+                    IllegalArgumentException.class,
+                    () -> new AnnotationBasedEventCriteriaResolver(EntityWithZeroArgEventCriteriaBuilder.class));
+            assertEquals(
+                    "Method annotated with @EventCriteriaBuilder must have exactly one parameter. Violating method: buildCriteria()",
+                    exception.getMessage()
+            );
+        }
+
+
+        @EventSourcedEntity
+        class EntityWithZeroArgEventCriteriaBuilder {
+
+            @EventCriteriaBuilder
+            public static EventCriteria buildCriteria() {
+                return EventCriteria.anyEvent();
+            }
+        }
+
+        @Test
+        void testEventCriteriaBuilderWithVoidReturnValueThrowsException() {
+            var exception = Assertions.assertThrows(
+                    IllegalArgumentException.class,
+                    () -> new AnnotationBasedEventCriteriaResolver(EntityWithVoidReturnValue.class));
+            assertEquals(
+                    "Method annotated with @EventCriteriaBuilder must return an EventCriteria. Violating method: buildCriteria(java.lang.String)",
+                    exception.getMessage()
+            );
+        }
+
+        @EventSourcedEntity
+        class EntityWithVoidReturnValue {
+
+            @EventCriteriaBuilder
+            public static void buildCriteria(String id) {
+            }
+        }
+
+        @Test
+        void testEventCriteriaBuilderWithNonEventSourcedEntityThrowsException() {
+            var exception = Assertions.assertThrows(
+                    IllegalArgumentException.class,
+                    () -> new AnnotationBasedEventCriteriaResolver(NonEventSourcedEntity.class));
+            assertEquals(
+                    "The given class is not an @EventSourcedEntity",
+                    exception.getMessage()
+            );
+        }
+
+        class NonEventSourcedEntity {
+
+        }
+
+        @Test
+        void testEventCriteriaBuilderWithDuplicatedParamterTypeThrowsException() {
+            var exception = Assertions.assertThrows(
+                    IllegalArgumentException.class,
+                    () -> new AnnotationBasedEventCriteriaResolver(EntityWithDuplicatedParameterType.class));
+            assertEquals(
+                    "Multiple @EventCriteriaBuilder methods found with the same parameter type: buildCriteriaOne(java.lang.String), buildCriteriaTwo(java.lang.String)",
+                    exception.getMessage()
+            );
+        }
+
+
+        @EventSourcedEntity
+        class EntityWithDuplicatedParameterType {
+
+            @EventCriteriaBuilder
+            public static EventCriteria buildCriteriaOne(String id) {
+                return EventCriteria.anyEvent();
+            }
+
+            @EventCriteriaBuilder
+            public static EventCriteria buildCriteriaTwo(String id) {
+                return EventCriteria.anyEvent();
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/eventsourcing/src/test/java/org/axonframework/eventsourcing/annotation/AnnotationBasedEventSourcingEntityRepositoryTest.java b/eventsourcing/src/test/java/org/axonframework/eventsourcing/annotation/AnnotationBasedEventSourcingEntityRepositoryTest.java
new file mode 100644
index 00000000000..1f64edb7959
--- /dev/null
+++ b/eventsourcing/src/test/java/org/axonframework/eventsourcing/annotation/AnnotationBasedEventSourcingEntityRepositoryTest.java
@@ -0,0 +1,301 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+import jakarta.annotation.Nonnull;
+import org.axonframework.commandhandling.CommandHandlingComponent;
+import org.axonframework.commandhandling.GenericCommandMessage;
+import org.axonframework.eventhandling.GenericEventMessage;
+import org.axonframework.eventsourcing.CriteriaResolver;
+import org.axonframework.eventsourcing.EventSourcingHandler;
+import org.axonframework.eventsourcing.eventstore.EventCriteria;
+import org.axonframework.eventsourcing.eventstore.SimpleEventStore;
+import org.axonframework.eventsourcing.eventstore.Tag;
+import org.axonframework.eventsourcing.eventstore.inmemory.AsyncInMemoryEventStorageEngine;
+import org.axonframework.messaging.MessageStream;
+import org.axonframework.messaging.MessageType;
+import org.axonframework.messaging.StubProcessingContext;
+import org.axonframework.messaging.unitofwork.AsyncUnitOfWork;
+import org.axonframework.messaging.unitofwork.ProcessingContext;
+import org.axonframework.modelling.SimpleStateManager;
+import org.axonframework.modelling.StateManager;
+import org.axonframework.modelling.command.StatefulCommandHandlingComponent;
+import org.axonframework.modelling.command.annotation.TargetEntityId;
+import org.junit.jupiter.api.*;
+import org.mockito.*;
+
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class AnnotationBasedEventSourcingEntityRepositoryTest {
+
+    @Test
+    void doesNotAcceptNonAnnotatedEntityClass() {
+        assertThrows(IllegalArgumentException.class, () -> new AnnotationBasedEventSourcingEntityRepository<>(
+                Mockito.mock(),
+                String.class,
+                String.class
+        ));
+    }
+
+    @Nested
+    class ClassName extends AnnotationBasedEntityMiniTestSuite {
+
+        ClassName() {
+            super(SimpleSequenceEntity.class, "SimpleSequenceEntity");
+        }
+    }
+
+    @Nested
+    class TagKey extends AnnotationBasedEntityMiniTestSuite {
+
+        TagKey() {
+            super(TagBasedSequenceEntity.class, "mySuperSpecialTag");
+        }
+
+        @EventSourcedEntity(tagKey = "mySuperSpecialTag")
+        static class TagBasedSequenceEntity extends AbstractSequenceEntity {
+
+        }
+    }
+
+
+    @Nested
+    class EventCriteriaBuilderMethod extends AnnotationBasedEntityMiniTestSuite {
+
+        EventCriteriaBuilderMethod() {
+            super(EventCriteriaBuilderSequenceEntity.class, "myCriteriaBuilderTag");
+        }
+
+        @EventSourcedEntity
+        static class EventCriteriaBuilderSequenceEntity extends AbstractSequenceEntity {
+
+            @EventCriteriaBuilder
+            public static EventCriteria buildCriteriaFor(String myString) {
+                return EventCriteria.match()
+                                    .eventsOfAnyType()
+                                    .withTags("myCriteriaBuilderTag", myString);
+            }
+        }
+    }
+
+    @Nested
+    class MetaAnnotated extends AnnotationBasedEntityMiniTestSuite {
+
+        MetaAnnotated() {
+            super(MetaAnnotatedSequenceEntity.class, "metaAnnotated");
+        }
+
+        @MetaAnnotatedEventSourcingEntity
+        static class MetaAnnotatedSequenceEntity extends AbstractSequenceEntity {
+
+        }
+    }
+
+    @Nested
+    class CustomEventCriteriaResolver extends AnnotationBasedEntityMiniTestSuite {
+
+        CustomEventCriteriaResolver() {
+            super(CustomEventCriteriaResolverSequenceEntity.class, "customCriteriaResolver");
+        }
+
+        @EventSourcedEntity(criteriaResolver = CustomCriteriaResolver.class)
+        static class CustomEventCriteriaResolverSequenceEntity extends AbstractSequenceEntity {
+
+        }
+
+        static class CustomCriteriaResolver implements CriteriaResolver<String> {
+
+            @Override
+            public EventCriteria apply(String id) {
+                return EventCriteria.match()
+                                    .eventsOfAnyType()
+                                    .withTags("customCriteriaResolver", id);
+            }
+        }
+    }
+
+    @Nested
+    class CustomCreator extends AnnotationBasedEntityMiniTestSuite {
+
+        CustomCreator() {
+            super(CustomCreatorSequenceEntity.class, "CustomCreatorSequenceEntity");
+        }
+
+        @EventSourcedEntity(entityFactory = CustomEntityFactory.class)
+        static class CustomCreatorSequenceEntity extends AbstractSequenceEntity {
+
+            private CustomCreatorSequenceEntity() {
+                throw new UnsupportedOperationException("Should not be called");
+            }
+
+            private CustomCreatorSequenceEntity(String one, String two, String three) {
+                // No-op
+            }
+        }
+
+        static class CustomEntityFactory implements EventSourcedEntityFactory<String, CustomCreatorSequenceEntity> {
+
+            @Override
+            public CustomCreatorSequenceEntity createEntity(@Nonnull Class<CustomCreatorSequenceEntity> entityType, @Nonnull String id) {
+                return new CustomCreatorSequenceEntity("one", "two", "three");
+            }
+        }
+    }
+
+    abstract class AnnotationBasedEntityMiniTestSuite {
+
+        private final SimpleEventStore eventStore;
+        private final AnnotationBasedEventSourcingEntityRepository<String, ? extends AbstractSequenceEntity> repository;
+        private final StateManager stateManager;
+
+        public AnnotationBasedEntityMiniTestSuite(Class<? extends AbstractSequenceEntity> entityClass,
+                                                  String expectedTagKey) {
+            this.eventStore = new SimpleEventStore(
+                    new AsyncInMemoryEventStorageEngine(),
+                    e -> {
+                        if (e.getPayload() instanceof MySequenceEvent mySequenceEvent) {
+                            return Set.of(Tag.of(expectedTagKey, mySequenceEvent.id()));
+                        }
+                        throw new IllegalArgumentException("Unsupported event type: " + e.getPayload().getClass());
+                    }
+            );
+            this.repository = new AnnotationBasedEventSourcingEntityRepository<>(
+                    eventStore,
+                    String.class,
+                    entityClass
+            );
+            this.stateManager = SimpleStateManager.builder("StateManager")
+                                                  .register(repository)
+                                                  .build();
+        }
+
+        @Test
+        void verifyEventSourcingWorksProperly() {
+            var component = StatefulCommandHandlingComponent
+                    .create("msch", stateManager)
+                    .subscribe(
+                            new MessageType(MyEntitySimpleCommand.class).qualifiedName(),
+                            (c, sm, ctx) -> {
+                                MyEntitySimpleCommand command = (MyEntitySimpleCommand) c.getPayload();
+                                AbstractSequenceEntity entity = sm.loadEntity(repository.entityType(),
+                                                                              command.myId(),
+                                                                              ctx)
+                                                                  .join();
+                                int newSequenceNumber = entity.getSequenceNumber() + 1;
+                                appendEvent(new MySequenceEvent(command.myId(), newSequenceNumber), ctx);
+                                return MessageStream.just(null);
+                            });
+
+            sendCommand(component, new MyEntitySimpleCommand("myId"));
+            verifySequenceNumber("myId", 0);
+            sendCommand(component, new MyEntitySimpleCommand("myId"));
+            verifySequenceNumber("myId", 1);
+
+            // Try out another ID that should not match
+            sendCommand(component, new MyEntitySimpleCommand("myId2"));
+            verifySequenceNumber("myId2", 0);
+
+            // And the other ID should not influence the original
+            sendCommand(component, new MyEntitySimpleCommand("myId"));
+            verifySequenceNumber("myId", 2);
+        }
+
+        private void verifySequenceNumber(String myId, int expected) {
+            AbstractSequenceEntity entity = stateManager
+                    .loadEntity(repository.entityType(), myId, new StubProcessingContext())
+                    .join();
+            assertEquals(expected, entity.getSequenceNumber());
+        }
+
+        private void appendEvent(Object event, ProcessingContext ctx) {
+            eventStore.transaction(ctx).appendEvent(
+                    new GenericEventMessage<>(new MessageType(event.getClass()), event)
+            );
+        }
+    }
+
+
+    protected <T> void sendCommand(
+            CommandHandlingComponent component,
+            T payload
+    ) {
+        AsyncUnitOfWork uow = new AsyncUnitOfWork();
+        uow.executeWithResult((context) -> {
+            GenericCommandMessage<T> command = new GenericCommandMessage<>(
+                    new MessageType(payload.getClass()),
+                    payload);
+            return component.handle(command, context).first().asCompletableFuture();
+        }).join();
+    }
+
+    record MyEntitySimpleCommand(
+            @TargetEntityId
+            String myId
+    ) {
+
+    }
+
+    @EventSourcedEntity
+    static class MyEntity {
+
+        private Integer sequenceNumber = -1;
+
+        @EventSourcingHandler
+        void handle(MySequenceEvent event) {
+            sequenceNumber = event.sequenceNumber();
+        }
+
+        Integer getSequenceNumber() {
+            return sequenceNumber;
+        }
+
+        @EventCriteriaBuilder
+        public static EventCriteria buildCriteriaFor(String myString) {
+            return EventCriteria.match()
+                                .eventsOfAnyType()
+                                .withTags("myTag", myString);
+        }
+    }
+
+    static class AbstractSequenceEntity {
+
+        private Integer sequenceNumber = -1;
+
+        @EventSourcingHandler
+        void handle(MySequenceEvent event) {
+            sequenceNumber = event.sequenceNumber();
+        }
+
+        Integer getSequenceNumber() {
+            return sequenceNumber;
+        }
+    }
+
+    @EventSourcedEntity
+    static class SimpleSequenceEntity extends AbstractSequenceEntity {
+
+    }
+
+    record MySequenceEvent(
+            String id,
+            Integer sequenceNumber
+    ) {
+
+    }
+}
\ No newline at end of file
diff --git a/eventsourcing/src/test/java/org/axonframework/eventsourcing/annotation/MetaAnnotatedEventSourcingEntity.java b/eventsourcing/src/test/java/org/axonframework/eventsourcing/annotation/MetaAnnotatedEventSourcingEntity.java
new file mode 100644
index 00000000000..fff182b1550
--- /dev/null
+++ b/eventsourcing/src/test/java/org/axonframework/eventsourcing/annotation/MetaAnnotatedEventSourcingEntity.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@EventSourcedEntity(tagKey = "metaAnnotated")
+public @interface MetaAnnotatedEventSourcingEntity {
+}
diff --git a/integrationtests/src/test/java/org/axonframework/integrationtests/testsuite/student/CompoundEntityIdentifierCommandHandlingComponentTest.java b/integrationtests/src/test/java/org/axonframework/integrationtests/testsuite/student/CompoundEntityIdentifierCommandHandlingComponentTest.java
index 7aa1489267b..cc63c5ff5a8 100644
--- a/integrationtests/src/test/java/org/axonframework/integrationtests/testsuite/student/CompoundEntityIdentifierCommandHandlingComponentTest.java
+++ b/integrationtests/src/test/java/org/axonframework/integrationtests/testsuite/student/CompoundEntityIdentifierCommandHandlingComponentTest.java
@@ -21,11 +21,7 @@
 import org.axonframework.commandhandling.annotation.CommandHandler;
 import org.axonframework.eventhandling.EventSink;
 import org.axonframework.eventhandling.GenericEventMessage;
-import org.axonframework.eventsourcing.AnnotationBasedEventStateApplier;
-import org.axonframework.eventsourcing.AsyncEventSourcingRepository;
-import org.axonframework.eventsourcing.EventStateApplier;
-import org.axonframework.eventsourcing.eventstore.EventCriteria;
-import org.axonframework.eventsourcing.eventstore.Tag;
+import org.axonframework.eventsourcing.annotation.AnnotationBasedEventSourcingEntityRepository;
 import org.axonframework.integrationtests.testsuite.student.commands.AssignMentorCommand;
 import org.axonframework.integrationtests.testsuite.student.common.StudentMentorModelIdentifier;
 import org.axonframework.integrationtests.testsuite.student.events.MentorAssignedToStudentEvent;
@@ -48,25 +44,10 @@
  */
 class CompoundEntityIdentifierCommandHandlingComponentTest extends AbstractStudentTestSuite {
 
-    protected EventStateApplier<StudentMentorAssignment> studentMentorModelApplier = new AnnotationBasedEventStateApplier<>(
-            StudentMentorAssignment.class);
-
-
-    protected AsyncEventSourcingRepository<StudentMentorModelIdentifier, StudentMentorAssignment> mentorAssignmentRepository = new AsyncEventSourcingRepository<>(
-            StudentMentorModelIdentifier.class,
-            StudentMentorAssignment.class,
+    protected AnnotationBasedEventSourcingEntityRepository<StudentMentorModelIdentifier, StudentMentorAssignment> mentorAssignmentRepository = new AnnotationBasedEventSourcingEntityRepository<>(
             eventStore,
-            id ->
-                    EventCriteria.either(
-                            EventCriteria.match()
-                                         .eventsOfTypes(MentorAssignedToStudentEvent.class.getName())
-                                         .withTags(new Tag("Student", id.menteeId())),
-                            EventCriteria.match()
-                                         .eventsOfTypes(MentorAssignedToStudentEvent.class.getName())
-                                         .withTags(new Tag("Student", id.mentorId()))
-                    ),
-            studentMentorModelApplier,
-            StudentMentorAssignment::new
+            StudentMentorModelIdentifier.class,
+            StudentMentorAssignment.class
     );
 
     @Override
@@ -135,8 +116,7 @@ void canHandleCommandThatTargetsMultipleModelsViaStatefulCommandHandler() {
                             if (assignment.isMenteeHasMentor()) {
                                 throw new IllegalArgumentException("Mentee already has a mentor");
                             }
-                            appendEvent(context,
-                                        new MentorAssignedToStudentEvent(payload.mentorId(), payload.menteeId()));
+                            appendEvent(context, new MentorAssignedToStudentEvent(payload.mentorId(), payload.menteeId()));
                             return MessageStream.empty().cast();
                         });
 
@@ -149,7 +129,6 @@ class CompoundModelAnnotatedCommandHandler {
         @CommandHandler
         public void handle(AssignMentorCommand command,
                            @InjectEntity StudentMentorAssignment assignment,
-                           EventSink eventSink,
                            ProcessingContext context
         ) {
             if (assignment.isMentorHasMentee()) {
@@ -159,11 +138,7 @@ public void handle(AssignMentorCommand command,
                 throw new IllegalArgumentException("Mentee already has a mentor");
             }
 
-            eventSink.publish(context,
-                              new GenericEventMessage<>(
-                                      new MessageType(MentorAssignedToStudentEvent.class),
-                                      new MentorAssignedToStudentEvent(command.mentorId(), command.menteeId())
-                              ));
+            appendEvent(context, new MentorAssignedToStudentEvent(command.mentorId(), command.menteeId()));
         }
     }
 }
\ No newline at end of file
diff --git a/integrationtests/src/test/java/org/axonframework/integrationtests/testsuite/student/state/StudentMentorAssignment.java b/integrationtests/src/test/java/org/axonframework/integrationtests/testsuite/student/state/StudentMentorAssignment.java
index e0b331e2cc4..f0d57aa5a13 100644
--- a/integrationtests/src/test/java/org/axonframework/integrationtests/testsuite/student/state/StudentMentorAssignment.java
+++ b/integrationtests/src/test/java/org/axonframework/integrationtests/testsuite/student/state/StudentMentorAssignment.java
@@ -17,9 +17,14 @@
 package org.axonframework.integrationtests.testsuite.student.state;
 
 import org.axonframework.eventsourcing.EventSourcingHandler;
+import org.axonframework.eventsourcing.annotation.EventCriteriaBuilder;
+import org.axonframework.eventsourcing.annotation.EventSourcedEntity;
+import org.axonframework.eventsourcing.eventstore.EventCriteria;
+import org.axonframework.eventsourcing.eventstore.Tag;
 import org.axonframework.integrationtests.testsuite.student.common.StudentMentorModelIdentifier;
 import org.axonframework.integrationtests.testsuite.student.events.MentorAssignedToStudentEvent;
 
+@EventSourcedEntity
 public class StudentMentorAssignment {
 
     private StudentMentorModelIdentifier identifier;
@@ -47,4 +52,16 @@ public void handle(MentorAssignedToStudentEvent event) {
             menteeHasMentor = true;
         }
     }
+
+    @EventCriteriaBuilder
+    public static EventCriteria resolveCriteria(StudentMentorModelIdentifier id) {
+        return EventCriteria.either(
+                EventCriteria.match()
+                             .eventsOfTypes(MentorAssignedToStudentEvent.class.getName())
+                             .withTags(new Tag("Student", id.menteeId())),
+                EventCriteria.match()
+                             .eventsOfTypes(MentorAssignedToStudentEvent.class.getName())
+                             .withTags(new Tag("Student", id.mentorId()))
+        );
+    }
 }
diff --git a/messaging/src/main/java/org/axonframework/common/ConstructorUtils.java b/messaging/src/main/java/org/axonframework/common/ConstructorUtils.java
new file mode 100644
index 00000000000..ad5c752bf8e
--- /dev/null
+++ b/messaging/src/main/java/org/axonframework/common/ConstructorUtils.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.common;
+
+import jakarta.annotation.Nonnull;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * Utility class for constructing objects using reflection.
+ * <p>
+ * Utility functions in this class do not create instances directly. Instead, they provide reusable functions that can
+ * be used to construct a multitude of instances of the same type. It is advisable to cache this function for reuse.
+ *
+ * @author Mitchell Herrijgers
+ * @since 5.0.0
+ */
+public class ConstructorUtils {
+
+    /**
+     * Returns a function that constructs an instance of the given type using the zero-argument constructor. If the type
+     * does not have a zero-argument constructor, an {@link IllegalArgumentException} is thrown.
+     *
+     * @param type The type of object to construct. Must have a zero-argument constructor.
+     * @param <T>  The type of object to construct.
+     * @return A function that constructs an instance of the given type using the zero-argument constructor.
+     */
+    public static <T> Supplier<T> getConstructorFunctionWithZeroArguments(@Nonnull Class<T> type) {
+        try {
+            Constructor<T> constructor = type.getDeclaredConstructor();
+            ReflectionUtils.ensureAccessible(constructor);
+            return () -> doConstructionWithOptionalArgument(type, null, constructor);
+        } catch (Exception e) {
+            throw new IllegalArgumentException(
+                    "No suitable constructor found for entity of type [%s] with zero arguments"
+                            .formatted(type.getName()));
+        }
+    }
+
+    /**
+     * Returns a function that constructs an instance of the given type using the constructor that accepts an argument
+     * of the given class, or alternatively using a zero-argument constructor. If neither is available, an
+     * {@link IllegalArgumentException} is thrown.
+     *
+     * @param type          The type of object to construct. Must have a constructor that accepts an argument of the
+     *                      given class, or a zero-argument constructor.
+     * @param argumentClass The class of the argument to pass to the constructor.
+     * @param <T>           The type of object to construct.
+     * @param <A>           The type of the argument to pass to the constructor.
+     * @return A function that constructs an instance of the given type using the constructor that accepts an argument
+     * of the given class, or alternatively using a zero-argument constructor.
+     */
+    public static <T, A> Function<A, T> factoryForTypeWithOptionalArgument(
+            @Nonnull Class<T> type,
+            @Nonnull Class<? extends A> argumentClass) {
+        Constructor<T> constructor = getConstructorWithOptionalArgumentOfType(type, argumentClass);
+        return arg -> doConstructionWithOptionalArgument(type, arg, constructor);
+    }
+
+    @SuppressWarnings("unchecked")
+    private static <T> Constructor<T> getConstructorWithOptionalArgumentOfType(@Nonnull Class<T> type,
+                                                                               @Nonnull Class<?> argument) {
+        return (Constructor<T>) Arrays
+                .stream(type.getDeclaredConstructors())
+                .filter(constructor -> constructorHasZeroOrExactlyThisArgument(constructor, argument))
+                .max(Comparator.comparingInt(Constructor::getParameterCount))
+                .orElseThrow(() -> new IllegalArgumentException(
+                        "No suitable constructor found for entity of type [%s] with optional argument of type [%s]"
+                                .formatted(type.getName(), argument.getName())));
+    }
+
+    private static boolean constructorHasZeroOrExactlyThisArgument(@Nonnull Constructor<?> constructor,
+                                                                   @Nonnull Class<?> argument) {
+        if (constructor.getParameterCount() == 0) {
+            return true;
+        }
+        // We want a constructor that can accept the argument
+        return constructor.getParameterCount() == 1 && constructor.getParameterTypes()[0].isAssignableFrom(argument);
+    }
+
+    private static <T> T doConstructionWithOptionalArgument(Class<T> type,
+                                                            Object argument,
+                                                            Constructor<T> constructor) {
+        try {
+            ReflectionUtils.ensureAccessible(constructor);
+            if (constructor.getParameterCount() == 0) {
+                return constructor.newInstance();
+            }
+            return constructor.newInstance(argument);
+        } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
+            throw new IllegalArgumentException("Error creating %s".formatted(type), e);
+        }
+    }
+
+    private ConstructorUtils() {
+        // Utility class
+    }
+}
diff --git a/messaging/src/main/java/org/axonframework/common/ReflectionUtils.java b/messaging/src/main/java/org/axonframework/common/ReflectionUtils.java
index 3a76db114da..1266df21b33 100644
--- a/messaging/src/main/java/org/axonframework/common/ReflectionUtils.java
+++ b/messaging/src/main/java/org/axonframework/common/ReflectionUtils.java
@@ -17,6 +17,7 @@
 package org.axonframework.common;
 
 import java.lang.reflect.AccessibleObject;
+import java.lang.reflect.Constructor;
 import java.lang.reflect.Executable;
 import java.lang.reflect.Field;
 import java.lang.reflect.InvocationTargetException;
@@ -27,12 +28,14 @@
 import java.lang.reflect.Type;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 
 import static org.axonframework.common.ObjectUtils.getOrDefault;
diff --git a/messaging/src/test/java/org/axonframework/common/ConstructorUtilsTest.java b/messaging/src/test/java/org/axonframework/common/ConstructorUtilsTest.java
new file mode 100644
index 00000000000..526c78779c2
--- /dev/null
+++ b/messaging/src/test/java/org/axonframework/common/ConstructorUtilsTest.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.common;
+
+import org.junit.jupiter.api.*;
+
+import java.util.function.Supplier;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class ConstructorUtilsTest {
+
+    @Nested
+    class OptionalArgument {
+
+        @Test
+        void canNotGetConstructorWithWrongArgumentType() {
+            assertThrows(IllegalArgumentException.class,
+                         () -> ConstructorUtils.factoryForTypeWithOptionalArgument(
+                                 ClassWithOnlyIntegerConstructor.class,
+                                 String.class
+                         )
+            );
+        }
+
+        @Test
+        void canConstructObjectWithOneArgConstructorForInteger() {
+            var function = ConstructorUtils.factoryForTypeWithOptionalArgument(
+                    ClassWithOnlyIntegerConstructor.class,
+                    Integer.class
+            );
+
+            ClassWithOnlyIntegerConstructor instance1 = function.apply(1);
+            ClassWithOnlyIntegerConstructor instance2 = function.apply(2);
+            ClassWithOnlyIntegerConstructor instance3 = function.apply(3);
+
+            assertEquals(1, instance1.getValue());
+            assertEquals(2, instance2.getValue());
+            assertEquals(3, instance3.getValue());
+        }
+
+        @Test
+        void canConstructObjectWithOneArgConstructorForString() {
+            var function = ConstructorUtils.factoryForTypeWithOptionalArgument(
+                    TestClassWithOnlyStringConstructor.class,
+                    String.class
+            );
+
+            TestClassWithOnlyStringConstructor instance1 = function.apply("1");
+            TestClassWithOnlyStringConstructor instance2 = function.apply("2");
+            TestClassWithOnlyStringConstructor instance3 = function.apply("3");
+
+            assertEquals("1", instance1.getValue());
+            assertEquals("2", instance2.getValue());
+            assertEquals("3", instance3.getValue());
+        }
+
+        @Test
+        void canConstructObjectWithZeroArgumentEvenWithOptionalArgument() {
+            var function = ConstructorUtils.factoryForTypeWithOptionalArgument(
+                    TestClassWithOnlyZeroArgConstructor.class,
+                    String.class
+            );
+
+            TestClassWithOnlyZeroArgConstructor instance1 = function.apply("1");
+            TestClassWithOnlyZeroArgConstructor instance2 = function.apply("2");
+            TestClassWithOnlyZeroArgConstructor instance3 = function.apply("3");
+
+            assertNotNull(instance1);
+            assertNotNull(instance2);
+            assertNotNull(instance3);
+
+            assertNotSame(instance1, instance2);
+            assertNotSame(instance1, instance3);
+            assertNotSame(instance2, instance3);
+        }
+
+        @Test
+        void fallsBackToNoArgConstructorIfTypeDoesNotMatch() {
+            var function = ConstructorUtils.factoryForTypeWithOptionalArgument(
+                    ClassWithStringAndNoArgConstructor.class,
+                    Integer.class
+            );
+            ClassWithStringAndNoArgConstructor instance1 = function.apply(1);
+            ClassWithStringAndNoArgConstructor instance2 = function.apply(2);
+            ClassWithStringAndNoArgConstructor instance3 = function.apply(3);
+
+            assertNotNull(instance1);
+            assertNotNull(instance2);
+            assertNotNull(instance3);
+
+            assertEquals("default", instance1.getValue());
+            assertEquals("default", instance2.getValue());
+            assertEquals("default", instance3.getValue());
+
+            assertNotSame(instance1, instance2);
+            assertNotSame(instance1, instance3);
+            assertNotSame(instance2, instance3);
+        }
+    }
+
+
+    @Nested
+    class ZeroArgConstructor {
+
+        @Test
+        void canGetZeroArgConstructorAndUseItToConstructMultipleTimes() {
+            Supplier<TestClassWithOnlyZeroArgConstructor> supplier = ConstructorUtils.getConstructorFunctionWithZeroArguments(
+                    TestClassWithOnlyZeroArgConstructor.class
+            );
+            TestClassWithOnlyZeroArgConstructor instance1 = supplier.get();
+            assertInstanceOf(TestClassWithOnlyZeroArgConstructor.class, instance1);
+            TestClassWithOnlyZeroArgConstructor instance2 = supplier.get();
+            assertInstanceOf(TestClassWithOnlyZeroArgConstructor.class, instance2);
+            TestClassWithOnlyZeroArgConstructor instance3 = supplier.get();
+            assertInstanceOf(TestClassWithOnlyZeroArgConstructor.class, instance3);
+
+            assertNotSame(instance1, instance2);
+            assertNotSame(instance1, instance3);
+            assertNotSame(instance2, instance3);
+        }
+
+        @Test
+        void canNotGetZeroArgConstructorIfItDoesntExist() {
+            assertThrows(
+                    IllegalArgumentException.class,
+                    () -> ConstructorUtils.getConstructorFunctionWithZeroArguments(
+                            TestClassWithOnlyStringConstructor.class
+                    )
+            );
+        }
+    }
+
+
+    @Nested
+    class Records {
+
+
+        @Test
+        void canConstructRecordClassWithProvidedOptionalArgument() {
+            var function = ConstructorUtils.factoryForTypeWithOptionalArgument(
+                    RecordClassWithOneArgConstructor.class,
+                    String.class
+            );
+            assertNotNull(function);
+            RecordClassWithOneArgConstructor instance1 = function.apply("test1");
+            RecordClassWithOneArgConstructor instance2 = function.apply("test2");
+            RecordClassWithOneArgConstructor instance3 = function.apply("test3");
+            assertEquals("test1", instance1.value());
+            assertEquals("test2", instance2.value());
+            assertEquals("test3", instance3.value());
+        }
+
+        @Test
+        void canNotConstructRecordClassWithTwoArgConstructor() {
+            assertThrows(
+                    IllegalArgumentException.class,
+                    () -> ConstructorUtils.factoryForTypeWithOptionalArgument(
+                            RecordClassWithTwoArgConstructor.class,
+                            String.class
+                    )
+            );
+        }
+
+        @Test
+        void canConstructRecordWithOverloadedConstructorIfHasOneArgument() {
+            var function = ConstructorUtils.factoryForTypeWithOptionalArgument(
+                    RecordClassWithOverloadedConstructor.class,
+                    Integer.class
+            );
+
+            assertNotNull(function);
+            RecordClassWithOverloadedConstructor instance1 = function.apply(1);
+            RecordClassWithOverloadedConstructor instance2 = function.apply(2);
+            RecordClassWithOverloadedConstructor instance3 = function.apply(3);
+
+            assertEquals("1", instance1.value());
+            assertEquals(1, instance1.intValue());
+            assertEquals("2", instance2.value());
+            assertEquals(2, instance2.intValue());
+            assertEquals("3", instance3.value());
+            assertEquals(3, instance3.intValue());
+        }
+
+
+        public record RecordClassWithOneArgConstructor(String value) {
+
+        }
+
+        public record RecordClassWithTwoArgConstructor(String value, Integer intValue) {
+
+        }
+
+        public record RecordClassWithOverloadedConstructor(String value, Integer intValue) {
+
+            public RecordClassWithOverloadedConstructor(Integer intValue) {
+                this(String.valueOf(intValue), intValue);
+            }
+        }
+    }
+
+    // Shared Test classes
+    @SuppressWarnings("ClassCanBeRecord")
+    public static class TestClassWithOnlyStringConstructor {
+
+        private final String value;
+
+        public TestClassWithOnlyStringConstructor(String value) {
+            this.value = value;
+        }
+
+        public String getValue() {
+            return value;
+        }
+    }
+
+    @SuppressWarnings("ClassCanBeRecord")
+    public static class ClassWithOnlyIntegerConstructor {
+
+        private final Integer value;
+
+        public ClassWithOnlyIntegerConstructor(Integer value) {
+            this.value = value;
+        }
+
+        public Integer getValue() {
+            return value;
+        }
+    }
+
+    public static class TestClassWithOnlyZeroArgConstructor {
+
+        public TestClassWithOnlyZeroArgConstructor() {
+        }
+    }
+
+    public static class ClassWithStringAndNoArgConstructor {
+
+        private final String value;
+
+        public ClassWithStringAndNoArgConstructor(String value) {
+            this.value = value;
+        }
+
+        public ClassWithStringAndNoArgConstructor() {
+            this.value = "default";
+        }
+
+        public String getValue() {
+            return value;
+        }
+    }
+}
\ No newline at end of file
diff --git a/modelling/src/main/java/org/axonframework/modelling/command/annotation/InjectEntityParameterResolverFactory.java b/modelling/src/main/java/org/axonframework/modelling/command/annotation/InjectEntityParameterResolverFactory.java
index 399727aa00c..d32ccca5727 100644
--- a/modelling/src/main/java/org/axonframework/modelling/command/annotation/InjectEntityParameterResolverFactory.java
+++ b/modelling/src/main/java/org/axonframework/modelling/command/annotation/InjectEntityParameterResolverFactory.java
@@ -28,7 +28,7 @@
 import java.lang.reflect.ParameterizedType;
 
 import static java.util.Objects.requireNonNull;
-import static org.axonframework.common.ReflectionUtils.ensureAccessible;
+import static org.axonframework.common.ConstructorUtils.getConstructorFunctionWithZeroArguments;
 
 /**
  * {@link ParameterResolverFactory} implementation that provides {@link ParameterResolver ParameterResolvers} for
@@ -97,16 +97,10 @@ private static EntityIdResolver<?> getEntityIdResolver(InjectEntity annotation)
         if (annotation.idProperty() != null && !annotation.idProperty().isEmpty()) {
             return new PropertyBasedEntityIdResolver(annotation.idProperty());
         }
-        return constructEntityIdResolver(annotation);
-    }
-
-    private static EntityIdResolver<?> constructEntityIdResolver(InjectEntity annotation) {
         try {
-            var constructor = annotation.idResolver().getDeclaredConstructor();
-            ensureAccessible(constructor);
-            return constructor.newInstance();
+            return getConstructorFunctionWithZeroArguments(annotation.idResolver()).get();
         } catch (Exception e) {
-            throw new RuntimeException(e);
+            throw new IllegalStateException("Failed to instantiate id resolver: " + annotation.idResolver().getName(), e);
         }
     }
 }


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This pull request introduces support for annotated Event Sourced Entities by implementing the AnnotationBasedEventSourcingEntityRepository along with associated criteria resolution and entity creation utilities. Key changes include:
- Refactoring of entity ID resolver construction using ConstructorUtils.
- Introduction of the ConstructorUtils utility for streamlined reflective instantiation.
- New tests and enhancements for annotated criteria resolution and custom entity creation.

### Reviewed Changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 1 comment.

<details>
<summary>Show a summary per file</summary>

| File | Description |
| ---- | ----------- |
| modelling/src/main/java/org/axonframework/modelling/command/annotation/InjectEntityParameterResolverFactory.java | Refactored resolver construction to use ConstructorUtils for zero-argument constructor lookup. |
| messaging/src/main/java/org/axonframework/common/ReflectionUtils.java | Added missing imports to support new constructor utility functionality. |
| messaging/src/main/java/org/axonframework/common/ConstructorUtils.java | New utility class for reflective construction with zero or optional arguments. |
| integrationtests/* | Updated tests to leverage the AnnotationBasedEventSourcingEntityRepository and its configuration options. |
| eventsourcing/src/test/java/org/axonframework/eventsourcing/annotation/* | New tests for criteria resolution, custom creator, and meta-annotation support. |
| eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/* | Added new annotations, repository implementation, criteria resolver, and creator classes to support annotated event sourcing. |
| eventsourcing/src/main/java/org/axonframework/eventsourcing/eventstore/Tag.java | Added a convenience static method for Tag creation. |
</details>





state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T12:37:16Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR introduces annotation‐based support for event‐sourced entities by adding the AnnotationBasedEventSourcingEntityRepository along with various utilities and tests around event criteria resolution and entity creation. The key changes include:
- Replacing direct reflective instantiation with cached constructor functions in Resolver and Creator utilities.
- Adding annotation-based configuration and tests for event criteria resolution via @EventCriteriaBuilder.
- Updating integration tests to leverage the new repository and associated annotations.

### Reviewed Changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 1 comment.

<details>
<summary>Show a summary per file</summary>

| File | Description |
| ---- | ----------- |
| modelling/src/main/java/org/axonframework/modelling/command/annotation/InjectEntityParameterResolverFactory.java | Refactored entity ID resolver instantiation using constructor utility. |
| messaging/src/main/java/org/axonframework/common/ReflectionUtils.java | Added necessary imports to support new reflective utilities. |
| messaging/src/main/java/org/axonframework/common/ConstructorUtils.java | Introduced reusable functions for constructor lookup and instantiation. |
| integrationtests/src/test/** | Updated/added tests for event sourcing entity behavior and criteria resolution. |
| eventsourcing/src/** | Introduced several new annotation-based classes, annotations, and tests for event-sourced entities. |
</details>





state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T12:39:17Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR adds annotation‐based event sourcing support by introducing the AnnotationBasedEventSourcingEntityRepository along with supporting utilities for entity instantiation and event criteria resolution. Key changes include refactoring entity resolver instantiation using ConstructorUtils, introducing new annotation interfaces and test cases for event criteria resolution, and updating tests to use the new repository.

### Reviewed Changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 2 comments.

| File | Description |
| ---- | ----------- |
| modelling/src/main/java/org/axonframework/modelling/command/annotation/InjectEntityParameterResolverFactory.java | Replaces manual constructor accessibility handling with ConstructorUtils for cleaner entity id resolver instantiation. |
| messaging/src/main/java/org/axonframework/common/ConstructorUtils.java | New utility class to create constructor functions using zero-argument or optional argument constructors. |
| integrationtests/src/test/java/org/axonframework/integrationtests/testsuite/student/* | Updates test cases to utilize the new AnnotationBasedEventSourcingEntityRepository and event criteria resolution via annotations. |
| eventsourcing/* | Introduces new annotation interfaces and implementations (e.g. EventSourcedEntity, EventCriteriaBuilder) along with extensive test coverage for criteria resolution and entity creation. |




state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T12:54:37Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This pull request introduces the AnnotationBasedEventSourcingEntityRepository and supporting utility and test classes to enable annotation‐based configuration for event sourcing entities. Key changes include:
- Integration of a new repository implementation that leverages the @EventSourcedEntity annotation for configuring event criteria resolution and entity creation.
- Introduction of utility classes such as ConstructorUtils and ConstructorBasedEventSourcedEntityCreator to facilitate instance construction.
- Updated test suites covering various configuration scenarios and ensuring exception conditions are handled as expected.

### Reviewed Changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 1 comment.

<details>
<summary>Show a summary per file</summary>

| File | Description |
| ---- | ----------- |
| modelling/src/main/java/org/axonframework/modelling/command/annotation/InjectEntityParameterResolverFactory.java | Updates constructor instantiation using ConstructorUtils for improved accessibility handling. |
| messaging/src/main/java/org/axonframework/common/ReflectionUtils.java | Adds additional imports to support constructor resolution. |
| messaging/src/main/java/org/axonframework/common/ConstructorUtils.java | Introduces utility functions for constructor-based instantiation with optional argument support. |
| integrationtests/.../StudentMentorAssignment.java | Adds @EventSourcedEntity annotation and a criteria builder method for event filtering. |
| integrationtests/.../CompoundEntityIdentifierCommandHandlingComponentTest.java | Updates repository instantiation to use the new AnnotationBasedEventSourcingEntityRepository and replaces legacy eventSink usage. |
| eventsourcing/src/test/.../*.java | Adds comprehensive tests covering entity criteria resolution and error conditions. |
| eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/*.java | Introduces new annotations and supporting classes for annotation-based event sourcing entity configuration. |
</details>



<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/AnnotationBasedEventCriteriaResolver.java:130**
* Consider including the input value and the invoked method's signature in the exception message when the @EventCriteriaBuilder method returns null. This extra context will improve debugging clarity if the builder produces an unexpected result.
```
if (!(result instanceof EventCriteria)) {
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T13:32:58Z
user: login: MateuszNaKodach
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T11:11:59Z
user: login: smcvb
body: Bunch of comments, nothing really major, expect perhaps for the purpose of the `AnnotationBasedEventSourcingEntityRepository` in sight of the `EntityBuilders` coming soon.

Concerning @abuijze pointer on having an annotation next to the `@EventSourcingHandler` - I think it a nice addition, to allow sourcing to differ slightly, but I am not entirely sure whether that's the default approach. Furthermore, I do feel for @MateuszNaKodach his point whether _that_ should be milestone material.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T12:33:33Z
user: login: CodeDrivenMitch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T12:48:02Z
user: login: CodeDrivenMitch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T12:56:57Z
user: login: CodeDrivenMitch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T12:58:10Z
user: login: CodeDrivenMitch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T12:59:01Z
user: login: CodeDrivenMitch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T13:02:35Z
user: login: CodeDrivenMitch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T13:03:13Z
user: login: CodeDrivenMitch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T13:06:02Z
user: login: CodeDrivenMitch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T13:06:07Z
user: login: CodeDrivenMitch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T13:06:39Z
user: login: CodeDrivenMitch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T13:08:02Z
user: login: CodeDrivenMitch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T13:09:07Z
user: login: smcvb
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T15:14:02Z
user: login: smcvb
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T15:15:17Z
user: login: smcvb
body: Although I am curious how the discussion tomorrow concerning the event-sourcing handler specific annotation is going to turn this PR around, but for now, I am actually okay with how all this looks. Two nits remaining, but they aren't big enough to block over.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T15:20:04Z
user: login: CodeDrivenMitch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T15:53:25Z
user: login: CodeDrivenMitch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T15:54:23Z

Review comments:
diff_hunk: @@ -0,0 +1,153 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+import jakarta.annotation.Nonnull;
+import org.axonframework.common.ReflectionUtils;
+import org.axonframework.common.annotation.AnnotationUtils;
+import org.axonframework.common.infra.ComponentDescriptor;
+import org.axonframework.common.infra.DescribableComponent;
+import org.axonframework.eventsourcing.CriteriaResolver;
+import org.axonframework.eventsourcing.eventstore.EventCriteria;
+import org.axonframework.eventsourcing.eventstore.Tag;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Annotation-based {@link CriteriaResolver} implementation which resolves {@link EventCriteria} based on the given
+ * {@code id} for loading the entity. This is the default when using the {@link EventSourcedEntity} annotation.
+ * <p>
+ * There are various ways to define how the {@link EventCriteria} should be resolved. In order of precedence:
+ * <ol>
+ *     <li>
+ *         By defining a static method in the entity class annotated with {@link EventCriteriaBuilder} which returns an
+ *         {@link EventCriteria} and accepts the {@code id} as a parameter. This method should be static and return an
+ *         {@link EventCriteria}. Multiple methods can be defined with different id types, and the first matching method
+ *         will be used.
+ *     </li>
+ *     <li>
+ *         If no matching {@link EventCriteriaBuilder} is found, the {@link EventSourcedEntity#tagName()} will be used as the tag key, and the {@link Object#toString()} of the id will be used as value.
+ *     </li>
+ *     <li>
+ *         If the {@link EventSourcedEntity#tagName()} is empty, the {@link Class#getSimpleName()} of the entity will be used as tag key, and the {@link Object#toString()} of the id will be used as value.
+ *     </li>
+ * </ol>
+ *
+ * @author Mitchell Herrijgers
+ * @see EventSourcedEntity
+ * @since 5.0.0
+ */
+public class AnnotationBasedEventCriteriaResolver implements CriteriaResolver<Object>, DescribableComponent {
+
+    private final Class<?> entityType;
+    private final String tagName;
+    private final Map<Class<?>, Method> builderMap;
+
+    /**
+     * Initialize the resolver for the given {@code entityType}. The entity type should be annotated with
+     * {@link EventSourcedEntity}, or this resolver will throw an {@link IllegalArgumentException}.
+     * <p>
+     * Will check for methods annotated with {@link EventCriteriaBuilder} and store them in a map for later use. If one
+     * of the methods is invalid as defined in the Javadoc of {@link EventCriteriaBuilder}, an
+     * {@link IllegalArgumentException} will be thrown.
+     *
+     * @param entityType The entity type to resolve criteria for
+     */
+    public AnnotationBasedEventCriteriaResolver(Class<?> entityType) {
+        this.entityType = entityType;
+
+
+        Map<String, Object> attributes = AnnotationUtils
+                .findAnnotationAttributes(entityType, EventSourcedEntity.class)
+                .orElseThrow(() -> new IllegalArgumentException("The given class it not an @EventSourcedEntity"));
+
path: eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/AnnotationBasedEventCriteriaResolver.java
user: login: Copilot
body: The error message contains a typo: 'it not' should be 'is not'.
```suggestion
                .orElseThrow(() -> new IllegalArgumentException("The given class is not an @EventSourcedEntity"));
```
created_at: 2025-03-27T12:37:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,153 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+import jakarta.annotation.Nonnull;
+import org.axonframework.common.ReflectionUtils;
+import org.axonframework.common.annotation.AnnotationUtils;
+import org.axonframework.common.infra.ComponentDescriptor;
+import org.axonframework.common.infra.DescribableComponent;
+import org.axonframework.eventsourcing.CriteriaResolver;
+import org.axonframework.eventsourcing.eventstore.EventCriteria;
+import org.axonframework.eventsourcing.eventstore.Tag;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Annotation-based {@link CriteriaResolver} implementation which resolves {@link EventCriteria} based on the given
+ * {@code id} for loading the entity. This is the default when using the {@link EventSourcedEntity} annotation.
+ * <p>
+ * There are various ways to define how the {@link EventCriteria} should be resolved. In order of precedence:
+ * <ol>
+ *     <li>
+ *         By defining a static method in the entity class annotated with {@link EventCriteriaBuilder} which returns an
+ *         {@link EventCriteria} and accepts the {@code id} as a parameter. This method should be static and return an
+ *         {@link EventCriteria}. Multiple methods can be defined with different id types, and the first matching method
+ *         will be used.
+ *     </li>
+ *     <li>
+ *         If no matching {@link EventCriteriaBuilder} is found, the {@link EventSourcedEntity#tagKey()} will be used as the tag key, and the {@link Object#toString()} of the id will be used as value.
+ *     </li>
+ *     <li>
+ *         If the {@link EventSourcedEntity#tagKey()} is empty, the {@link Class#getSimpleName()} of the entity will be used as tag key, and the {@link Object#toString()} of the id will be used as value.
+ *     </li>
+ * </ol>
+ *
+ * @author Mitchell Herrijgers
+ * @see EventSourcedEntity
+ * @since 5.0.0
+ */
+public class AnnotationBasedEventCriteriaResolver implements CriteriaResolver<Object>, DescribableComponent {
+
+    private final Class<?> entityType;
+    private final String tagKey;
+    private final Map<Class<?>, Method> builderMap;
+
+    /**
+     * Initialize the resolver for the given {@code entityType}. The entity type should be annotated with
+     * {@link EventSourcedEntity}, or this resolver will throw an {@link IllegalArgumentException}.
+     * <p>
+     * Will check for methods annotated with {@link EventCriteriaBuilder} and store them in a map for later use. If one
+     * of the methods is invalid as defined in the Javadoc of {@link EventCriteriaBuilder}, an
+     * {@link IllegalArgumentException} will be thrown.
+     *
+     * @param entityType The entity type to resolve criteria for
+     */
+    public AnnotationBasedEventCriteriaResolver(Class<?> entityType) {
+        this.entityType = entityType;
+
+
+        Map<String, Object> attributes = AnnotationUtils
+                .findAnnotationAttributes(entityType, EventSourcedEntity.class)
+                .orElseThrow(() -> new IllegalArgumentException("The given class is not an @EventSourcedEntity"));
+
+        String annotationTagKey = (String) attributes.get("tagKey");
+        this.tagKey = annotationTagKey.isEmpty() ? null : annotationTagKey;
+
+        Set<Method> eventCriteriaBuilders = Arrays.stream(entityType.getMethods())
+                                                  .filter(m -> m.isAnnotationPresent(EventCriteriaBuilder.class))
+                                                  .collect(Collectors.toSet());
+        eventCriteriaBuilders.forEach(AnnotationBasedEventCriteriaResolver::validateEventCriteriaBuilderMethod);
+
+        this.builderMap = eventCriteriaBuilders.stream()
path: eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/AnnotationBasedEventCriteriaResolver.java
user: login: Copilot
body: If multiple @EventCriteriaBuilder methods with the same parameter type exist, the collectors.toMap call will throw an exception without a clear error message. Consider handling duplicate builder methods explicitly to provide more guidance to users.
created_at: 2025-03-27T12:39:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -97,16 +97,6 @@ private static EntityIdResolver<?> getEntityIdResolver(InjectEntity annotation)
         if (annotation.idProperty() != null && !annotation.idProperty().isEmpty()) {
             return new PropertyBasedEntityIdResolver(annotation.idProperty());
         }
-        return constructEntityIdResolver(annotation);
-    }
-
-    private static EntityIdResolver<?> constructEntityIdResolver(InjectEntity annotation) {
-        try {
-            var constructor = annotation.idResolver().getDeclaredConstructor();
-            ensureAccessible(constructor);
-            return constructor.newInstance();
-        } catch (Exception e) {
-            throw new RuntimeException(e);
-        }
+        return getConstructorFunctionWithZeroArguments(annotation.idResolver()).get();
path: modelling/src/main/java/org/axonframework/modelling/command/annotation/InjectEntityParameterResolverFactory.java
user: login: Copilot
body: [nitpick] Consider adding error handling or additional context in case the supplier returned by getConstructorFunctionWithZeroArguments fails to produce an instance, to help diagnose issues during id resolver instantiation.
```suggestion
        try {
            return getConstructorFunctionWithZeroArguments(annotation.idResolver()).get();
        } catch (Exception e) {
            throw new IllegalStateException("Failed to instantiate id resolver: " + annotation.idResolver().getName(), e);
        }
```
created_at: 2025-03-27T12:54:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,194 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+import org.axonframework.eventsourcing.eventstore.EventCriteria;
+import org.junit.jupiter.api.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class AnnotationBasedEventCriteriaResolverTest {
+
+    @Test
+    void resolvedRightBuilderMethodForDifferentIdTypes() {
+        var resolver = new AnnotationBasedEventCriteriaResolver(FunctionalEventSourcedEntity.class);
+
+        var criteriaString = resolver.resolve("id");
+        assertEquals(EventCriteria.match().eventsOfAnyType().withTags("aggregateIdentifierOfString", "id"), criteriaString);
+
+        var criteriaLong = resolver.resolve(1L);
+        assertEquals(EventCriteria.match().eventsOfAnyType().withTags("aggregateIdentifierOfLong", "1"), criteriaLong);
+    }
+
+    @Test
+    void throwsWhenEventCriteriaBuilderReturnsNull() {
+        var resolver = new AnnotationBasedEventCriteriaResolver(FunctionalEventSourcedEntity.class);
+
+        var exception = Assertions.assertThrows(
+                IllegalArgumentException.class,
+                () -> resolver.resolve(1)
+        );
+        assertEquals(
+                "The @EventCriteriaBuilder method returned null. The method must return a non-null EventCriteria. Violating method: buildCriteria(java.lang.Integer)",
+                exception.getMessage()
+        );
+    }
+
+    @Test
+    void usesTagKeyPropertyIfNoEventCriteriaBuilderMatches() {
+        var resolver = new AnnotationBasedEventCriteriaResolver(FunctionalEventSourcedEntity.class);
+
+        var criteria = resolver.resolve(0.0);
+        assertEquals(EventCriteria.match().eventsOfAnyType().withTags("fallbackTagKey", "0.0"), criteria);
+    }
+
+    @EventSourcedEntity(tagKey = "fallbackTagKey")
+    class FunctionalEventSourcedEntity {
+
+        @EventCriteriaBuilder
+        public static EventCriteria buildCriteria(String id) {
+            return EventCriteria.match().eventsOfAnyType().withTags("aggregateIdentifierOfString", id);
+        }
+
+        @EventCriteriaBuilder
+        public static EventCriteria buildCriteria(Long id) {
+            return EventCriteria.match().eventsOfAnyType().withTags("aggregateIdentifierOfLong", id.toString());
+        }
+
+        @EventCriteriaBuilder
+        public static EventCriteria buildCriteria(Integer id) {
+            return null;
+        }
+    }
+
+    @Test
+    void usesSimpleClassNameAsTagNameIfNoTagKeyPropertyIsSet() {
+        var resolver = new AnnotationBasedEventCriteriaResolver(DefaultEventSourcedEntity.class);
+
+        var criteria = resolver.resolve("id");
+        assertEquals(EventCriteria.match().eventsOfAnyType().withTags("DefaultEventSourcedEntity", "id"), criteria);
+    }
+
+    @EventSourcedEntity()
+    class DefaultEventSourcedEntity {
+    }
+
+    @Nested
+    class ConfigurationProblems {
+
+        @Test
+        void testEventCriteriaBuilderWithNonStaticBuilderThrowsException() {
+            var exception = Assertions.assertThrows(
+                    IllegalArgumentException.class,
+                    () -> new AnnotationBasedEventCriteriaResolver(EntityWithNonStaticEventCriteriaBuilder.class));
+            assertEquals(
+                    "Method annotated with @EventCriteriaBuilder must be static. Violating method: buildCriteria(java.lang.String)",
+                    exception.getMessage()
+            );
+        }
+
+        @EventSourcedEntity
+        class EntityWithNonStaticEventCriteriaBuilder {
+
+            @EventCriteriaBuilder
+            public EventCriteria buildCriteria(String id) {
+                return EventCriteria.match().eventsOfAnyType().withTags("aggregateIdentifier", id);
+            }
+        }
+
+        @Test
+        void testEventCriteriaBuilderWithZeroArgBuilderThrowsException() {
+            var exception = Assertions.assertThrows(
+                    IllegalArgumentException.class,
+                    () -> new AnnotationBasedEventCriteriaResolver(EntityWithZeroArgEventCriteriaBuilder.class));
+            assertEquals(
+                    "Method annotated with @EventCriteriaBuilder must have exactly one parameter. Violating method: buildCriteria()",
+                    exception.getMessage()
+            );
+        }
+
+
+        @EventSourcedEntity
+        class EntityWithZeroArgEventCriteriaBuilder {
+
+            @EventCriteriaBuilder
+            public static EventCriteria buildCriteria() {
+                return EventCriteria.anyEvent();
+            }
+        }
+
+        @Test
+        void testEventCriteriaBuilderWithVoidReturnValueThrowsException() {
+            var exception = Assertions.assertThrows(
+                    IllegalArgumentException.class,
+                    () -> new AnnotationBasedEventCriteriaResolver(EntityWithVoidReturnValue.class));
+            assertEquals(
+                    "Method annotated with @EventCriteriaBuilder must return an EventCriteria. Violating method: buildCriteria(java.lang.String)",
+                    exception.getMessage()
+            );
+        }
+
+        @EventSourcedEntity
+        class EntityWithVoidReturnValue {
+
+            @EventCriteriaBuilder
+            public static void buildCriteria(String id) {
+            }
+        }
+
+        @Test
+        void testEventCriteriaBuilderWithNonEventSourcedEntityThrowsException() {
+            var exception = Assertions.assertThrows(
+                    IllegalArgumentException.class,
+                    () -> new AnnotationBasedEventCriteriaResolver(NonEventSourcedEntity.class));
+            assertEquals(
+                    "The given class is not an @EventSourcedEntity",
+                    exception.getMessage()
+            );
+        }
+
+        class NonEventSourcedEntity {
+
+        }
+
+        @Test
+        void testEventCriteriaBuilderWithDuplicatedParamterTypeThrowsException() {
+            var exception = Assertions.assertThrows(
+                    IllegalArgumentException.class,
+                    () -> new AnnotationBasedEventCriteriaResolver(EntityWithDuplicatedParamterType.class));
+            assertEquals(
+                    "Multiple @EventCriteriaBuilder methods found with the same parameter type: buildCriteriaTwo(java.lang.String), buildCriteriaOne(java.lang.String)",
+                    exception.getMessage()
+            );
+        }
+
+
+        @EventSourcedEntity
+        class EntityWithDuplicatedParamterType {
path: eventsourcing/src/test/java/org/axonframework/eventsourcing/annotation/AnnotationBasedEventCriteriaResolverTest.java
user: login: Copilot
body: There is a typo in the class name 'EntityWithDuplicatedParamterType'; consider renaming it to 'EntityWithDuplicatedParameterType' for improved clarity.
created_at: 2025-03-27T12:54:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,136 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+import jakarta.annotation.Nonnull;
+import org.axonframework.common.ConstructorUtils;
+import org.axonframework.common.annotation.AnnotationUtils;
+import org.axonframework.common.infra.ComponentDescriptor;
+import org.axonframework.eventsourcing.AnnotationBasedEventStateApplier;
+import org.axonframework.eventsourcing.AsyncEventSourcingRepository;
+import org.axonframework.eventsourcing.CriteriaResolver;
+import org.axonframework.eventsourcing.eventstore.AsyncEventStore;
+import org.axonframework.messaging.unitofwork.ProcessingContext;
+import org.axonframework.modelling.repository.AsyncRepository;
+import org.axonframework.modelling.repository.ManagedEntity;
+
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+import static org.axonframework.common.ConstructorUtils.factoryForTypeWithOptionalArgumentInstance;
+
+/**
+ * Implementation of a {@link AsyncRepository} that configures an entity based on the configuration provided by the
+ * {@link EventSourcedEntity} annotation. Please refer to the {@link EventSourcedEntity} annotation for more information
+ * on the configuration options.
+ *
+ * @param <ID> The type of the identifier of the entity.
+ * @param <T>  The type of the entity.
+ */
+public class AnnotationBasedEventSourcingEntityRepository<ID, T> implements AsyncRepository.LifecycleManagement<ID, T> {
+
+    private final Class<ID> idType;
+    private final Class<T> entityType;
+    private final AsyncEventSourcingRepository<ID, T> repository;
+    private final EventSourcedEntityCreator<ID, T> creator;
+    private final CriteriaResolver<ID> criteriaResolver;
+    private final AnnotationBasedEventStateApplier<T> stateApplier;
+
+    /**
+     * Initialize a repository for the given {@code entityType} and {@code idType} using the provided
+     * {@code eventStore}.
+     *
+     * @param eventStore The event store to store and load events from.
+     * @param idType     The type of the identifier of the entity.
+     * @param entityType The type of the entity.
+     */
+    @SuppressWarnings("unchecked")
+    public AnnotationBasedEventSourcingEntityRepository(
+            AsyncEventStore eventStore,
+            Class<ID> idType,
+            Class<T> entityType
+    ) {
+        Map<String, Object> annotationAttributes = AnnotationUtils
+                .findAnnotationAttributes(entityType, EventSourcedEntity.class)
+                .orElseThrow(() -> new IllegalArgumentException("The given class is not an @EventSourcingEntity"));
+
+        this.idType = idType;
+        this.entityType = entityType;
+
+        var creatorType = (Class<EventSourcedEntityCreator<ID, T>>) annotationAttributes.get("entityCreator");
path: eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/AnnotationBasedEventSourcingEntityRepository.java
user: login: Copilot
body: Consider validating that the retrieved 'entityCreator' attribute is non-null and of the expected type before the unchecked cast to avoid potential ClassCastException at runtime.
```suggestion
        Object entityCreatorAttr = annotationAttributes.get("entityCreator");
        if (entityCreatorAttr == null || !(entityCreatorAttr instanceof Class<?>)) {
            throw new IllegalArgumentException("The 'entityCreator' attribute is missing or not of the expected type.");
        }
        var creatorType = (Class<EventSourcedEntityCreator<ID, T>>) entityCreatorAttr;
```
created_at: 2025-03-27T13:32:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,139 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.common;
+
+import jakarta.annotation.Nonnull;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * Utility class for constructing objects using reflection.
+ * <p>
+ * Utility functions in this class do not create instances directly. Instead, they provide reusable functions that can
+ * be used to construct a multitude of instances of the same type. It is advisable to cache this function for reuse.
+ *
+ * @author Mitchell Herrijgers
+ * @since 5.0.0
+ */
+public class ConstructorUtils {
+
+    /**
+     * Returns a function that constructs an instance of the given type using the zero-argument constructor. If the type
+     * does not have a zero-argument constructor, an {@link IllegalArgumentException} is thrown.
+     *
+     * @param type The type of object to construct. Must have a zero-argument constructor.
+     * @param <T>  The type of object to construct.
+     * @return A function that constructs an instance of the given type using the zero-argument constructor.
+     */
+    public static <T> Supplier<T> getConstructorFunctionWithZeroArguments(Class<T> type) {
+        try {
+            Constructor<T> constructor = type.getDeclaredConstructor();
+            ReflectionUtils.ensureAccessible(constructor);
+            return () -> doConstructionWithOptionalArgument(type, null, constructor);
+        } catch (Exception e) {
+            throw new IllegalArgumentException(
+                    "No suitable constructor found for entity of type [%s] with zero arguments"
+                            .formatted(type.getName()));
+        }
+    }
+
+    /**
+     * Returns a function that constructs an instance of the given type using the constructor that accepts an argument
+     * of the given class, or alternatively using a zero-argument constructor. If neither is available, an
+     * {@link IllegalArgumentException} is thrown.
+     *
+     * @param type          The type of object to construct. Must have a constructor that accepts an argument of the
+     *                      given class, or a zero-argument constructor.
+     * @param argumentClass The class of the argument to pass to the constructor.
+     * @param <T>           The type of object to construct.
+     * @param <A>           The type of the argument to pass to the constructor.
+     * @return A function that constructs an instance of the given type using the constructor that accepts an argument
+     * of the given class, or alternatively using a zero-argument constructor.
+     */
+    public static <T, A> Function<A, T> getConstructorFunctionWithOptionalArgumentClass(
+            @Nonnull Class<T> type,
+            @Nonnull Class<? extends A> argumentClass) {
+        Constructor<T> constructor = getConstructorWithOptionalArgumentOfType(type, argumentClass);
+        return arg -> doConstructionWithOptionalArgument(type, arg, constructor);
+    }
+
+    /**
+     * Returns a function that constructs an instance of the given type using the constructor that accepts an argument
+     * of the given instance's class, or alternatively using a zero-argument constructor. If neither is available, an
+     * {@link IllegalArgumentException} is thrown.
+     *
+     * @param type     The type of object to construct. Must have a constructor that accepts an argument of the given
+     *                 instance's class, or a zero-argument constructor.
+     * @param argument The instance to pass to the constructor.
+     * @param <T>      The type of object to construct.
+     * @param <A>      The type of the argument to pass to the constructor.
+     * @return A function that constructs an instance of the given type using the constructor that accepts an argument
+     * of the given instance's class, or alternatively using a zero-argument constructor.
+     */
+    public static <T, A> Supplier<T> factoryForTypeWithOptionalArgumentInstance(
+            @Nonnull Class<T> type,
+            @Nonnull A argument
+    ) {
+        Constructor<T> constructor = getConstructorWithOptionalArgumentOfType(type, argument.getClass());
+        return () -> doConstructionWithOptionalArgument(type, argument, constructor);
+    }
+
+    @SuppressWarnings("unchecked")
+    private static <T> Constructor<T> getConstructorWithOptionalArgumentOfType(@Nonnull Class<T> type,
+                                                                               @Nonnull Class<?> argument) {
+        return (Constructor<T>) Arrays
+                .stream(type.getDeclaredConstructors())
+                .filter(constructor -> constructorHasZeroOrExactlyThisArgument(constructor, argument))
+                .max(Comparator.comparingInt(Constructor::getParameterCount))
+                .orElseThrow(() -> new IllegalArgumentException(
+                        "No suitable constructor found for entity of type [%s] with optional argument of type [%s]"
+                                .formatted(type.getName(), argument.getName())));
+    }
+
+    private static boolean constructorHasZeroOrExactlyThisArgument(@Nonnull Constructor<?> constructor,
+                                                                   @Nonnull Class<?> argument) {
+        // Has no args, cool
path: messaging/src/main/java/org/axonframework/common/ConstructorUtils.java
user: login: MateuszNaKodach
body: I don't think this comment is needed :) 
created_at: 2025-03-28T09:56:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -48,25 +44,10 @@
  */
 class CompoundEntityIdentifierCommandHandlingComponentTest extends AbstractStudentTestSuite {
 
-    protected EventStateApplier<StudentMentorAssignment> studentMentorModelApplier = new AnnotationBasedEventStateApplier<>(
-            StudentMentorAssignment.class);
-
-
-    protected AsyncEventSourcingRepository<StudentMentorModelIdentifier, StudentMentorAssignment> mentorAssignmentRepository = new AsyncEventSourcingRepository<>(
-            StudentMentorModelIdentifier.class,
-            StudentMentorAssignment.class,
path: integrationtests/src/test/java/org/axonframework/integrationtests/testsuite/student/CompoundEntityIdentifierCommandHandlingComponentTest.java
user: login: MateuszNaKodach
body: I'm not sure if it's good we're replacing those non-annotated tests. Don't you think we should have test suite here and leave both? 
created_at: 2025-03-28T10:50:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+import jakarta.annotation.Nonnull;
+import org.axonframework.common.infra.ComponentDescriptor;
+import org.axonframework.common.infra.DescribableComponent;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+
+import static org.axonframework.common.ConstructorUtils.getConstructorFunctionWithOptionalArgumentClass;
+
+/**
+ * {@link EventSourcedEntityCreator} implementation which uses a constructor to create a new instance of an entity. The
+ * constructor can either have a single argument of the same type as the identifier, or no arguments at all.
+ *
+ * @param <M> The type of the entity to create.
+ * @author Mitchell Herrijgers
+ * @since 5.0.0
+ */
+public class ConstructorBasedEventSourcedEntityCreator<M>
+        implements EventSourcedEntityCreator<Object, M>, DescribableComponent {
+
+    private final Class<M> entityType;
+    private final Map<Class<?>, Function<Object, M>> constructorCache = new ConcurrentHashMap<>();
path: eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/ConstructorBasedEventSourcedEntityCreator.java
user: login: MateuszNaKodach
body: I think it's very unlikely to have too much object types, but just to be on the safe site and prevent too much memory usage I think we may have some limit for this cache (and others you've introduced), like 100 types or something. 

What do you think? Or maybe I'm just too worry :) 

created_at: 2025-03-28T11:07:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+import org.axonframework.eventsourcing.eventstore.EventCriteria;
+import org.axonframework.modelling.command.annotation.TargetEntityId;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to indicate that a method can be used to resolve the {@link TargetEntityId} to {@link EventCriteria}
path: eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/EventCriteriaBuilder.java
user: login: MateuszNaKodach
body: ```suggestion
 * Annotation to indicate that a method can be used to resolve the {@link EventCriteria} based on {@link TargetEntityId} 
```
It's easier to understand for me, but up to you. 
created_at: 2025-03-28T11:09:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+import org.axonframework.eventsourcing.eventstore.EventCriteria;
+import org.axonframework.modelling.command.annotation.TargetEntityId;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to indicate that a method can be used to resolve the {@link TargetEntityId} to {@link EventCriteria}
+ * instance for an {@link EventSourcedEntity}.
+ * <p>
+ * The method should be:
+ * <ul>
+ *     <li>A static method</li>
+ *     <li>Accept a single parameter of the type of the {@link TargetEntityId} of the {@link EventSourcedEntity}</li>
+ *     <li>Return an {@link EventCriteria} instance</li>
+ * </ul>
+ * <p>
+ * In all other cases, an exception will be thrown during initialization of the
+ * {@link AnnotationBasedEventSourcingEntityRepository}.
+ *
+ * @author Mitchell Herrijgers
+ * @see AnnotationBasedEventSourcingEntityRepository
path: eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/EventCriteriaBuilder.java
user: login: MateuszNaKodach
body: ```suggestion
 * @see AnnotationBasedEventSourcingEntityRepository
 * @see TargetEntityId
```
created_at: 2025-03-28T11:09:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+import org.axonframework.eventsourcing.CriteriaResolver;
+import org.axonframework.eventsourcing.EventSourcingRepository;
+import org.axonframework.eventsourcing.eventstore.EventCriteria;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to configure several aspects of an event-sourced entity. This annotation is required to construct a
+ * {@link AnnotationBasedEventSourcingEntityRepository}.
+ * <p>
+ * While loading the entity from the {@link EventSourcingRepository}, the provided {@code id} needs to be translated to
+ * an {@link EventCriteria} instance to load the correct events. Unless overridden, this translation is done by the
+ * {@link AnnotationBasedEventCriteriaResolver}. So, {@link EventCriteria} will be resolved in the following order:
+ * <ol>
+ *     <li>
+ *         Using the custom {@link #criteriaResolver()}. The provided class should implement the {@link CriteriaResolver},
+ *         and have a no-arg constructor, or a 1-arg constructor with the {@link Class} of the entity as parameter.
+ *     </li>
+ *     <li>
+ *         By defining a static method in the entity class annotated with {@link EventCriteriaBuilder} which returns an
+ *         {@link EventCriteria} and accepts the {@code id} as a parameter. This method should be static and return an
+ *         {@link EventCriteria}. Multiple methods can be defined with different id types, and the first matching method
+ *         will be used.
+ *     </li>
+ *     <li>
+ *         If no matching {@link EventCriteriaBuilder} is found, the {@link EventSourcedEntity#tagKey()} will be used as the tag key, and the {@link Object#toString()} of the id will be used as value.
+ *     </li>
+ *     <li>
+ *         If the {@link EventSourcedEntity#tagKey()} is empty, the {@link Class#getSimpleName()} of the entity will be used as tag key, and the {@link Object#toString()} of the id will be used as value.
path: eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/EventSourcedEntity.java
user: login: MateuszNaKodach
body: We still haven't decided on the Tag format, so I'm still not sure if it could be class name or (for example) "lower cased class name". 
created_at: 2025-03-28T11:11:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -83,4 +83,8 @@ public static Context addToContext(Context context, Set<Tag> tags) {
     public static Optional<Set<Tag>> fromContext(Context context) {
         return Optional.ofNullable(context.getResource(RESOURCE_KEY));
     }
+
+    public static Tag of(String key, String value) {
path: eventsourcing/src/main/java/org/axonframework/eventsourcing/eventstore/Tag.java
user: login: smcvb
body: I am missing JavaDoc here and a test for this method. Lastly, I think it's good to annotated the parameters with @NonNull, just as the constructor parameters.
created_at: 2025-03-31T11:56:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -48,25 +44,10 @@
  */
 class CompoundEntityIdentifierCommandHandlingComponentTest extends AbstractStudentTestSuite {
 
-    protected EventStateApplier<StudentMentorAssignment> studentMentorModelApplier = new AnnotationBasedEventStateApplier<>(
-            StudentMentorAssignment.class);
-
-
-    protected AsyncEventSourcingRepository<StudentMentorModelIdentifier, StudentMentorAssignment> mentorAssignmentRepository = new AsyncEventSourcingRepository<>(
-            StudentMentorModelIdentifier.class,
-            StudentMentorAssignment.class,
path: integrationtests/src/test/java/org/axonframework/integrationtests/testsuite/student/CompoundEntityIdentifierCommandHandlingComponentTest.java
user: login: smcvb
body: I agree we should have both...But that could just as well be an exercise I do in my PR or in a follow-up of my PR wherein I do all the configuration stuff. So, if Mitchell has time, awesome. If not, I'll do this.
created_at: 2025-03-31T11:58:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,139 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.common;
+
+import jakarta.annotation.Nonnull;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * Utility class for constructing objects using reflection.
+ * <p>
+ * Utility functions in this class do not create instances directly. Instead, they provide reusable functions that can
+ * be used to construct a multitude of instances of the same type. It is advisable to cache this function for reuse.
+ *
+ * @author Mitchell Herrijgers
+ * @since 5.0.0
+ */
+public class ConstructorUtils {
+
+    /**
+     * Returns a function that constructs an instance of the given type using the zero-argument constructor. If the type
+     * does not have a zero-argument constructor, an {@link IllegalArgumentException} is thrown.
+     *
+     * @param type The type of object to construct. Must have a zero-argument constructor.
+     * @param <T>  The type of object to construct.
+     * @return A function that constructs an instance of the given type using the zero-argument constructor.
+     */
+    public static <T> Supplier<T> getConstructorFunctionWithZeroArguments(Class<T> type) {
path: messaging/src/main/java/org/axonframework/common/ConstructorUtils.java
user: login: smcvb
body: ```suggestion
    public static <T> Supplier<T> getConstructorFunctionWithZeroArguments(@Nonull Class<T> type) {
```
created_at: 2025-03-31T12:00:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,139 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.common;
+
+import jakarta.annotation.Nonnull;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * Utility class for constructing objects using reflection.
+ * <p>
+ * Utility functions in this class do not create instances directly. Instead, they provide reusable functions that can
+ * be used to construct a multitude of instances of the same type. It is advisable to cache this function for reuse.
+ *
+ * @author Mitchell Herrijgers
+ * @since 5.0.0
+ */
+public class ConstructorUtils {
path: messaging/src/main/java/org/axonframework/common/ConstructorUtils.java
user: login: smcvb
body: Some tests for this new utility class would be nice. Although the tests are potentially cumbersome, as this util class might become a cornerstone of a lot of other code, it should be validated thoroughly.
created_at: 2025-03-31T12:00:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+/**
+ * Functional interface towards creating a new instance of an entity of the given {@code entityType} and the given
+ * {@code id}.
+ *
+ * @param <ID> The type of the identifier of the entity to create.
+ * @param <M>  The type of the entity to create.
+ */
path: eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/EventSourcedEntityCreator.java
user: login: smcvb
body: This functional interface deserves your name on it, as well as a since tag :-)

By the way, thanks for adding! It bothered me slightly in my entity builder PR that I had to set the factory method as a plain `Function`.
created_at: 2025-03-31T12:07:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+/**
+ * Functional interface towards creating a new instance of an entity of the given {@code entityType} and the given
+ * {@code id}.
+ *
+ * @param <ID> The type of the identifier of the entity to create.
+ * @param <M>  The type of the entity to create.
+ */
path: eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/EventSourcedEntityCreator.java
user: login: smcvb
body: That said, is this an entity creator or an entity factory?
created_at: 2025-03-31T12:07:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+/**
+ * Functional interface towards creating a new instance of an entity of the given {@code entityType} and the given
+ * {@code id}.
+ *
+ * @param <ID> The type of the identifier of the entity to create.
+ * @param <M>  The type of the entity to create.
+ */
+@FunctionalInterface
+public interface EventSourcedEntityCreator<ID, M> {
+
+    /**
+     * Creates a new instance of an entity of the given {@code entityType} and {@code idType}.
+     *
path: eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/EventSourcedEntityCreator.java
user: login: smcvb
body: This JavaDoc isn't complete. I am missing the parameters and the return value.
created_at: 2025-03-31T12:07:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+/**
+ * Functional interface towards creating a new instance of an entity of the given {@code entityType} and the given
+ * {@code id}.
+ *
+ * @param <ID> The type of the identifier of the entity to create.
+ * @param <M>  The type of the entity to create.
path: eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/EventSourcedEntityCreator.java
user: login: smcvb
body: Perhaps we should switch `M` for `E` to align with "entity" instead of "model."
created_at: 2025-03-31T12:08:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+import org.axonframework.eventsourcing.CriteriaResolver;
+import org.axonframework.eventsourcing.EventSourcingRepository;
+import org.axonframework.eventsourcing.eventstore.EventCriteria;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to configure several aspects of an event-sourced entity. This annotation is required to construct a
+ * {@link AnnotationBasedEventSourcingEntityRepository}.
+ * <p>
+ * While loading the entity from the {@link EventSourcingRepository}, the provided {@code id} needs to be translated to
+ * an {@link EventCriteria} instance to load the correct events. Unless overridden, this translation is done by the
+ * {@link AnnotationBasedEventCriteriaResolver}. So, {@link EventCriteria} will be resolved in the following order:
+ * <ol>
+ *     <li>
+ *         Using the custom {@link #criteriaResolver()}. The provided class should implement the {@link CriteriaResolver},
+ *         and have a no-arg constructor, or a 1-arg constructor with the {@link Class} of the entity as parameter.
+ *     </li>
+ *     <li>
+ *         By defining a static method in the entity class annotated with {@link EventCriteriaBuilder} which returns an
+ *         {@link EventCriteria} and accepts the {@code id} as a parameter. This method should be static and return an
+ *         {@link EventCriteria}. Multiple methods can be defined with different id types, and the first matching method
+ *         will be used.
+ *     </li>
+ *     <li>
+ *         If no matching {@link EventCriteriaBuilder} is found, the {@link EventSourcedEntity#tagKey()} will be used as the tag key, and the {@link Object#toString()} of the id will be used as value.
+ *     </li>
+ *     <li>
+ *         If the {@link EventSourcedEntity#tagKey()} is empty, the {@link Class#getSimpleName()} of the entity will be used as tag key, and the {@link Object#toString()} of the id will be used as value.
path: eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/EventSourcedEntity.java
user: login: smcvb
body: Wouldn't hurt to refer to issue [#3326](https://github.com/AxonFramework/AxonFramework/issues/3326) here indeed. Once we have the format, we'd need to change documentation in several places already. Such a number reference will make that easier.
created_at: 2025-03-31T12:10:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+import jakarta.annotation.Nonnull;
+import org.axonframework.common.infra.ComponentDescriptor;
+import org.axonframework.common.infra.DescribableComponent;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+
+import static org.axonframework.common.ConstructorUtils.getConstructorFunctionWithOptionalArgumentClass;
+
+/**
+ * {@link EventSourcedEntityCreator} implementation which uses a constructor to create a new instance of an entity. The
+ * constructor can either have a single argument of the same type as the identifier, or no arguments at all.
+ *
+ * @param <M> The type of the entity to create.
+ * @author Mitchell Herrijgers
+ * @since 5.0.0
+ */
+public class ConstructorBasedEventSourcedEntityCreator<M>
+        implements EventSourcedEntityCreator<Object, M>, DescribableComponent {
+
+    private final Class<M> entityType;
+    private final Map<Class<?>, Function<Object, M>> constructorCache = new ConcurrentHashMap<>();
+
+    public ConstructorBasedEventSourcedEntityCreator(Class<M> entityType) {
path: eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/ConstructorBasedEventSourcedEntityCreator.java
user: login: smcvb
body: I am missing JavaDoc on the constructor, as well as the `@Nonnull` annotation and it's validation on the parameter.
created_at: 2025-03-31T12:11:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+import jakarta.annotation.Nonnull;
+import org.axonframework.common.infra.ComponentDescriptor;
+import org.axonframework.common.infra.DescribableComponent;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+
+import static org.axonframework.common.ConstructorUtils.getConstructorFunctionWithOptionalArgumentClass;
+
+/**
+ * {@link EventSourcedEntityCreator} implementation which uses a constructor to create a new instance of an entity. The
+ * constructor can either have a single argument of the same type as the identifier, or no arguments at all.
+ *
+ * @param <M> The type of the entity to create.
+ * @author Mitchell Herrijgers
+ * @since 5.0.0
+ */
+public class ConstructorBasedEventSourcedEntityCreator<M>
+        implements EventSourcedEntityCreator<Object, M>, DescribableComponent {
+
+    private final Class<M> entityType;
+    private final Map<Class<?>, Function<Object, M>> constructorCache = new ConcurrentHashMap<>();
path: eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/ConstructorBasedEventSourcedEntityCreator.java
user: login: smcvb
body: I agree that it doesn't hurt to limit the size of the collection.
created_at: 2025-03-31T12:12:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,136 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+import jakarta.annotation.Nonnull;
+import org.axonframework.common.ConstructorUtils;
+import org.axonframework.common.annotation.AnnotationUtils;
+import org.axonframework.common.infra.ComponentDescriptor;
+import org.axonframework.eventsourcing.AnnotationBasedEventStateApplier;
+import org.axonframework.eventsourcing.AsyncEventSourcingRepository;
+import org.axonframework.eventsourcing.CriteriaResolver;
+import org.axonframework.eventsourcing.eventstore.AsyncEventStore;
+import org.axonframework.messaging.unitofwork.ProcessingContext;
+import org.axonframework.modelling.repository.AsyncRepository;
+import org.axonframework.modelling.repository.ManagedEntity;
+
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+import static org.axonframework.common.ConstructorUtils.factoryForTypeWithOptionalArgumentInstance;
+
+/**
+ * Implementation of a {@link AsyncRepository} that configures an entity based on the configuration provided by the
+ * {@link EventSourcedEntity} annotation. Please refer to the {@link EventSourcedEntity} annotation for more information
+ * on the configuration options.
+ *
+ * @param <ID> The type of the identifier of the entity.
+ * @param <T>  The type of the entity.
+ */
+public class AnnotationBasedEventSourcingEntityRepository<ID, T> implements AsyncRepository.LifecycleManagement<ID, T> {
path: eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/AnnotationBasedEventSourcingEntityRepository.java
user: login: smcvb
body: I am wondering. Is this a concern of the `Repository`, or should this be a different type of  `EntityBuilder`?
Granted, I only introduced that last Friday...

Although we worked in parallel and didn't discuss this together in enough detail, I figured it worthwhile to touch on the subject regardless. This `Repository` implementation only really does some constructor logic. Besides that, it's just a means to construct, and delegate, an `AsyncEventSourcingRepository`.
created_at: 2025-03-31T12:18:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+import jakarta.annotation.Nonnull;
+import org.axonframework.common.ReflectionUtils;
+import org.axonframework.common.annotation.AnnotationUtils;
+import org.axonframework.common.infra.ComponentDescriptor;
+import org.axonframework.common.infra.DescribableComponent;
+import org.axonframework.eventsourcing.CriteriaResolver;
+import org.axonframework.eventsourcing.eventstore.EventCriteria;
+import org.axonframework.eventsourcing.eventstore.Tag;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Annotation-based {@link CriteriaResolver} implementation which resolves {@link EventCriteria} based on the given
+ * {@code id} for loading the entity. This is the default when using the {@link EventSourcedEntity} annotation.
+ * <p>
+ * There are various ways to define how the {@link EventCriteria} should be resolved. In order of precedence:
+ * <ol>
+ *     <li>
+ *         By defining a static method in the entity class annotated with {@link EventCriteriaBuilder} which returns an
+ *         {@link EventCriteria} and accepts the {@code id} as a parameter. This method should be static and return an
+ *         {@link EventCriteria}. Multiple methods can be defined with different id types, and the first matching method
+ *         will be used.
+ *     </li>
+ *     <li>
+ *         If no matching {@link EventCriteriaBuilder} is found, the {@link EventSourcedEntity#tagKey()} will be used as the tag key, and the {@link Object#toString()} of the id will be used as value.
+ *     </li>
+ *     <li>
+ *         If the {@link EventSourcedEntity#tagKey()} is empty, the {@link Class#getSimpleName()} of the entity will be used as tag key, and the {@link Object#toString()} of the id will be used as value.
+ *     </li>
+ * </ol>
+ *
+ * @author Mitchell Herrijgers
+ * @see EventSourcedEntity
+ * @since 5.0.0
+ */
+public class AnnotationBasedEventCriteriaResolver implements CriteriaResolver<Object>, DescribableComponent {
+
+    private final Class<?> entityType;
+    private final String tagKey;
+    private final Map<Class<?>, Method> builderMap;
+
+    /**
+     * Initialize the resolver for the given {@code entityType}. The entity type should be annotated with
+     * {@link EventSourcedEntity}, or this resolver will throw an {@link IllegalArgumentException}.
+     * <p>
+     * Will check for methods annotated with {@link EventCriteriaBuilder} and store them in a map for later use. If one
+     * of the methods is invalid as defined in the Javadoc of {@link EventCriteriaBuilder}, an
+     * {@link IllegalArgumentException} will be thrown.
+     *
+     * @param entityType The entity type to resolve criteria for
+     */
+    public AnnotationBasedEventCriteriaResolver(Class<?> entityType) {
+        this.entityType = entityType;
path: eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/AnnotationBasedEventCriteriaResolver.java
user: login: smcvb
body: ```suggestion
    public AnnotationBasedEventCriteriaResolver(@Nonnull Class<?> entityType) {
        this.entityType = Objects.requireNonNull(entityType, "The entity type cannot be null.");
```
created_at: 2025-03-31T12:19:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+import jakarta.annotation.Nonnull;
+import org.axonframework.common.ReflectionUtils;
+import org.axonframework.common.annotation.AnnotationUtils;
+import org.axonframework.common.infra.ComponentDescriptor;
+import org.axonframework.common.infra.DescribableComponent;
+import org.axonframework.eventsourcing.CriteriaResolver;
+import org.axonframework.eventsourcing.eventstore.EventCriteria;
+import org.axonframework.eventsourcing.eventstore.Tag;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Annotation-based {@link CriteriaResolver} implementation which resolves {@link EventCriteria} based on the given
+ * {@code id} for loading the entity. This is the default when using the {@link EventSourcedEntity} annotation.
+ * <p>
+ * There are various ways to define how the {@link EventCriteria} should be resolved. In order of precedence:
+ * <ol>
+ *     <li>
+ *         By defining a static method in the entity class annotated with {@link EventCriteriaBuilder} which returns an
+ *         {@link EventCriteria} and accepts the {@code id} as a parameter. This method should be static and return an
+ *         {@link EventCriteria}. Multiple methods can be defined with different id types, and the first matching method
+ *         will be used.
+ *     </li>
+ *     <li>
+ *         If no matching {@link EventCriteriaBuilder} is found, the {@link EventSourcedEntity#tagKey()} will be used as the tag key, and the {@link Object#toString()} of the id will be used as value.
+ *     </li>
+ *     <li>
+ *         If the {@link EventSourcedEntity#tagKey()} is empty, the {@link Class#getSimpleName()} of the entity will be used as tag key, and the {@link Object#toString()} of the id will be used as value.
+ *     </li>
+ * </ol>
+ *
+ * @author Mitchell Herrijgers
+ * @see EventSourcedEntity
+ * @since 5.0.0
+ */
+public class AnnotationBasedEventCriteriaResolver implements CriteriaResolver<Object>, DescribableComponent {
+
+    private final Class<?> entityType;
+    private final String tagKey;
+    private final Map<Class<?>, Method> builderMap;
+
+    /**
+     * Initialize the resolver for the given {@code entityType}. The entity type should be annotated with
+     * {@link EventSourcedEntity}, or this resolver will throw an {@link IllegalArgumentException}.
+     * <p>
+     * Will check for methods annotated with {@link EventCriteriaBuilder} and store them in a map for later use. If one
+     * of the methods is invalid as defined in the Javadoc of {@link EventCriteriaBuilder}, an
+     * {@link IllegalArgumentException} will be thrown.
+     *
+     * @param entityType The entity type to resolve criteria for
path: eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/AnnotationBasedEventCriteriaResolver.java
user: login: smcvb
body: ```suggestion
     * @param entityType The entity type to resolve criteria for.
```
created_at: 2025-03-31T12:19:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+import jakarta.annotation.Nonnull;
+import org.axonframework.common.ReflectionUtils;
+import org.axonframework.common.annotation.AnnotationUtils;
+import org.axonframework.common.infra.ComponentDescriptor;
+import org.axonframework.common.infra.DescribableComponent;
+import org.axonframework.eventsourcing.CriteriaResolver;
+import org.axonframework.eventsourcing.eventstore.EventCriteria;
+import org.axonframework.eventsourcing.eventstore.Tag;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Annotation-based {@link CriteriaResolver} implementation which resolves {@link EventCriteria} based on the given
+ * {@code id} for loading the entity. This is the default when using the {@link EventSourcedEntity} annotation.
+ * <p>
+ * There are various ways to define how the {@link EventCriteria} should be resolved. In order of precedence:
+ * <ol>
+ *     <li>
+ *         By defining a static method in the entity class annotated with {@link EventCriteriaBuilder} which returns an
+ *         {@link EventCriteria} and accepts the {@code id} as a parameter. This method should be static and return an
+ *         {@link EventCriteria}. Multiple methods can be defined with different id types, and the first matching method
+ *         will be used.
+ *     </li>
+ *     <li>
+ *         If no matching {@link EventCriteriaBuilder} is found, the {@link EventSourcedEntity#tagKey()} will be used as the tag key, and the {@link Object#toString()} of the id will be used as value.
+ *     </li>
+ *     <li>
+ *         If the {@link EventSourcedEntity#tagKey()} is empty, the {@link Class#getSimpleName()} of the entity will be used as tag key, and the {@link Object#toString()} of the id will be used as value.
+ *     </li>
+ * </ol>
+ *
path: eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/AnnotationBasedEventCriteriaResolver.java
user: login: smcvb
body: Something that wasn't immediately apparent to me, is that this `AnnotationBasedEventCriteriaResolver` essentially groups several `CriteriaResolver` instances for the given `entityType`. I think it would be good to describe that in the class-level JavaDoc.
created_at: 2025-03-31T12:26:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,300 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+import org.axonframework.commandhandling.CommandHandlingComponent;
+import org.axonframework.commandhandling.GenericCommandMessage;
+import org.axonframework.eventhandling.GenericEventMessage;
+import org.axonframework.eventsourcing.CriteriaResolver;
+import org.axonframework.eventsourcing.EventSourcingHandler;
+import org.axonframework.eventsourcing.eventstore.EventCriteria;
+import org.axonframework.eventsourcing.eventstore.SimpleEventStore;
+import org.axonframework.eventsourcing.eventstore.Tag;
+import org.axonframework.eventsourcing.eventstore.inmemory.AsyncInMemoryEventStorageEngine;
+import org.axonframework.messaging.MessageStream;
+import org.axonframework.messaging.MessageType;
+import org.axonframework.messaging.StubProcessingContext;
+import org.axonframework.messaging.unitofwork.AsyncUnitOfWork;
+import org.axonframework.messaging.unitofwork.ProcessingContext;
+import org.axonframework.modelling.SimpleStateManager;
+import org.axonframework.modelling.StateManager;
+import org.axonframework.modelling.command.StatefulCommandHandlingComponent;
+import org.axonframework.modelling.command.annotation.TargetEntityId;
+import org.junit.jupiter.api.*;
+import org.mockito.*;
+
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class AnnotationBasedEventSourcingEntityRepositoryTest {
path: eventsourcing/src/test/java/org/axonframework/eventsourcing/annotation/AnnotationBasedEventSourcingEntityRepositoryTest.java
user: login: smcvb
body: **IF** we'd drop the class in favor of a `AnnotatedEventSourcedEntityBuilder` (based on PR #3355), then I still think there's value in this test class. But then, instead, for the `AnnotatedEventSourcedEntityBuilder`.
created_at: 2025-03-31T12:30:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,136 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+import jakarta.annotation.Nonnull;
+import org.axonframework.common.ConstructorUtils;
+import org.axonframework.common.annotation.AnnotationUtils;
+import org.axonframework.common.infra.ComponentDescriptor;
+import org.axonframework.eventsourcing.AnnotationBasedEventStateApplier;
+import org.axonframework.eventsourcing.AsyncEventSourcingRepository;
+import org.axonframework.eventsourcing.CriteriaResolver;
+import org.axonframework.eventsourcing.eventstore.AsyncEventStore;
+import org.axonframework.messaging.unitofwork.ProcessingContext;
+import org.axonframework.modelling.repository.AsyncRepository;
+import org.axonframework.modelling.repository.ManagedEntity;
+
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+import static org.axonframework.common.ConstructorUtils.factoryForTypeWithOptionalArgumentInstance;
+
+/**
+ * Implementation of a {@link AsyncRepository} that configures an entity based on the configuration provided by the
+ * {@link EventSourcedEntity} annotation. Please refer to the {@link EventSourcedEntity} annotation for more information
+ * on the configuration options.
+ *
+ * @param <ID> The type of the identifier of the entity.
+ * @param <T>  The type of the entity.
+ */
+public class AnnotationBasedEventSourcingEntityRepository<ID, T> implements AsyncRepository.LifecycleManagement<ID, T> {
path: eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/AnnotationBasedEventSourcingEntityRepository.java
user: login: CodeDrivenMitch
body: Yeah this could be another type of `EntityBuilder`. This notion does not exist in the 5.0 branch as you mentioned, so I cannot change this into that at this point in time. Whoever merges last, would have to move this code I guess :joy: Or, as they don't conflict, merging these two can be another PR I do after. 
created_at: 2025-03-31T12:48:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+import jakarta.annotation.Nonnull;
+import org.axonframework.common.infra.ComponentDescriptor;
+import org.axonframework.common.infra.DescribableComponent;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+
+import static org.axonframework.common.ConstructorUtils.getConstructorFunctionWithOptionalArgumentClass;
+
+/**
+ * {@link EventSourcedEntityCreator} implementation which uses a constructor to create a new instance of an entity. The
+ * constructor can either have a single argument of the same type as the identifier, or no arguments at all.
+ *
+ * @param <M> The type of the entity to create.
+ * @author Mitchell Herrijgers
+ * @since 5.0.0
+ */
+public class ConstructorBasedEventSourcedEntityCreator<M>
+        implements EventSourcedEntityCreator<Object, M>, DescribableComponent {
+
+    private final Class<M> entityType;
+    private final Map<Class<?>, Function<Object, M>> constructorCache = new ConcurrentHashMap<>();
path: eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/ConstructorBasedEventSourcedEntityCreator.java
user: login: CodeDrivenMitch
body: I think it's very unlikely that this becomes a problem, and limiting the cache has additional overhead. We would need to keep in mind which methods were last invoked, which would need cross-thread update of the cache for every call. 
I think it would hurt more than it would solve. And honestly, I don't want to implement [Caffeine](https://www.baeldung.com/java-caching-caffeine) manually.
created_at: 2025-03-31T12:56:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,300 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+import org.axonframework.commandhandling.CommandHandlingComponent;
+import org.axonframework.commandhandling.GenericCommandMessage;
+import org.axonframework.eventhandling.GenericEventMessage;
+import org.axonframework.eventsourcing.CriteriaResolver;
+import org.axonframework.eventsourcing.EventSourcingHandler;
+import org.axonframework.eventsourcing.eventstore.EventCriteria;
+import org.axonframework.eventsourcing.eventstore.SimpleEventStore;
+import org.axonframework.eventsourcing.eventstore.Tag;
+import org.axonframework.eventsourcing.eventstore.inmemory.AsyncInMemoryEventStorageEngine;
+import org.axonframework.messaging.MessageStream;
+import org.axonframework.messaging.MessageType;
+import org.axonframework.messaging.StubProcessingContext;
+import org.axonframework.messaging.unitofwork.AsyncUnitOfWork;
+import org.axonframework.messaging.unitofwork.ProcessingContext;
+import org.axonframework.modelling.SimpleStateManager;
+import org.axonframework.modelling.StateManager;
+import org.axonframework.modelling.command.StatefulCommandHandlingComponent;
+import org.axonframework.modelling.command.annotation.TargetEntityId;
+import org.junit.jupiter.api.*;
+import org.mockito.*;
+
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class AnnotationBasedEventSourcingEntityRepositoryTest {
path: eventsourcing/src/test/java/org/axonframework/eventsourcing/annotation/AnnotationBasedEventSourcingEntityRepositoryTest.java
user: login: CodeDrivenMitch
body: Agreed, in the PR that merges them, these tests should also be moved/merged
created_at: 2025-03-31T12:58:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -48,25 +44,10 @@
  */
 class CompoundEntityIdentifierCommandHandlingComponentTest extends AbstractStudentTestSuite {
 
-    protected EventStateApplier<StudentMentorAssignment> studentMentorModelApplier = new AnnotationBasedEventStateApplier<>(
-            StudentMentorAssignment.class);
-
-
-    protected AsyncEventSourcingRepository<StudentMentorModelIdentifier, StudentMentorAssignment> mentorAssignmentRepository = new AsyncEventSourcingRepository<>(
-            StudentMentorModelIdentifier.class,
-            StudentMentorAssignment.class,
path: integrationtests/src/test/java/org/axonframework/integrationtests/testsuite/student/CompoundEntityIdentifierCommandHandlingComponentTest.java
user: login: CodeDrivenMitch
body: When both PRs are merged I think there's a lot of value in making more examples in this test suite, and organizing them a bit more. For now that would cause too many merge conflicts
created_at: 2025-03-31T12:59:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+import jakarta.annotation.Nonnull;
+import org.axonframework.common.ReflectionUtils;
+import org.axonframework.common.annotation.AnnotationUtils;
+import org.axonframework.common.infra.ComponentDescriptor;
+import org.axonframework.common.infra.DescribableComponent;
+import org.axonframework.eventsourcing.CriteriaResolver;
+import org.axonframework.eventsourcing.eventstore.EventCriteria;
+import org.axonframework.eventsourcing.eventstore.Tag;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Annotation-based {@link CriteriaResolver} implementation which resolves {@link EventCriteria} based on the given
+ * {@code id} for loading the entity. This is the default when using the {@link EventSourcedEntity} annotation.
+ * <p>
+ * There are various ways to define how the {@link EventCriteria} should be resolved. In order of precedence:
+ * <ol>
+ *     <li>
+ *         By defining a static method in the entity class annotated with {@link EventCriteriaBuilder} which returns an
+ *         {@link EventCriteria} and accepts the {@code id} as a parameter. This method should be static and return an
+ *         {@link EventCriteria}. Multiple methods can be defined with different id types, and the first matching method
+ *         will be used.
+ *     </li>
+ *     <li>
+ *         If no matching {@link EventCriteriaBuilder} is found, the {@link EventSourcedEntity#tagKey()} will be used as the tag key, and the {@link Object#toString()} of the id will be used as value.
+ *     </li>
+ *     <li>
+ *         If the {@link EventSourcedEntity#tagKey()} is empty, the {@link Class#getSimpleName()} of the entity will be used as tag key, and the {@link Object#toString()} of the id will be used as value.
+ *     </li>
+ * </ol>
+ *
path: eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/AnnotationBasedEventCriteriaResolver.java
user: login: CodeDrivenMitch
body: Adding javadoc
created_at: 2025-03-31T13:02:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+import jakarta.annotation.Nonnull;
+import org.axonframework.common.infra.ComponentDescriptor;
+import org.axonframework.common.infra.DescribableComponent;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+
+import static org.axonframework.common.ConstructorUtils.getConstructorFunctionWithOptionalArgumentClass;
+
+/**
+ * {@link EventSourcedEntityCreator} implementation which uses a constructor to create a new instance of an entity. The
+ * constructor can either have a single argument of the same type as the identifier, or no arguments at all.
+ *
+ * @param <M> The type of the entity to create.
+ * @author Mitchell Herrijgers
+ * @since 5.0.0
+ */
+public class ConstructorBasedEventSourcedEntityCreator<M>
+        implements EventSourcedEntityCreator<Object, M>, DescribableComponent {
+
+    private final Class<M> entityType;
+    private final Map<Class<?>, Function<Object, M>> constructorCache = new ConcurrentHashMap<>();
+
+    public ConstructorBasedEventSourcedEntityCreator(Class<M> entityType) {
path: eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/ConstructorBasedEventSourcedEntityCreator.java
user: login: CodeDrivenMitch
body: Javadoc added
created_at: 2025-03-31T13:03:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+/**
+ * Functional interface towards creating a new instance of an entity of the given {@code entityType} and the given
+ * {@code id}.
+ *
+ * @param <ID> The type of the identifier of the entity to create.
+ * @param <M>  The type of the entity to create.
+ */
path: eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/EventSourcedEntityCreator.java
user: login: CodeDrivenMitch
body: Renamed it to factory
created_at: 2025-03-31T13:06:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+/**
+ * Functional interface towards creating a new instance of an entity of the given {@code entityType} and the given
+ * {@code id}.
+ *
+ * @param <ID> The type of the identifier of the entity to create.
+ * @param <M>  The type of the entity to create.
path: eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/EventSourcedEntityCreator.java
user: login: CodeDrivenMitch
body: Renamed to `E`
created_at: 2025-03-31T13:06:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+/**
+ * Functional interface towards creating a new instance of an entity of the given {@code entityType} and the given
+ * {@code id}.
+ *
+ * @param <ID> The type of the identifier of the entity to create.
+ * @param <M>  The type of the entity to create.
+ */
+@FunctionalInterface
+public interface EventSourcedEntityCreator<ID, M> {
+
+    /**
+     * Creates a new instance of an entity of the given {@code entityType} and {@code idType}.
+     *
path: eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/EventSourcedEntityCreator.java
user: login: CodeDrivenMitch
body: Javadoc added
created_at: 2025-03-31T13:06:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -83,4 +83,8 @@ public static Context addToContext(Context context, Set<Tag> tags) {
     public static Optional<Set<Tag>> fromContext(Context context) {
         return Optional.ofNullable(context.getResource(RESOURCE_KEY));
     }
+
+    public static Tag of(String key, String value) {
path: eventsourcing/src/main/java/org/axonframework/eventsourcing/eventstore/Tag.java
user: login: CodeDrivenMitch
body: Adding docs, annotations and checks
created_at: 2025-03-31T13:08:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,139 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.common;
+
+import jakarta.annotation.Nonnull;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * Utility class for constructing objects using reflection.
+ * <p>
+ * Utility functions in this class do not create instances directly. Instead, they provide reusable functions that can
+ * be used to construct a multitude of instances of the same type. It is advisable to cache this function for reuse.
+ *
+ * @author Mitchell Herrijgers
+ * @since 5.0.0
+ */
+public class ConstructorUtils {
path: messaging/src/main/java/org/axonframework/common/ConstructorUtils.java
user: login: CodeDrivenMitch
body: Fair enough, will write some nice tests for this. 
created_at: 2025-03-31T13:09:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,139 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.common;
+
+import jakarta.annotation.Nonnull;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * Utility class for constructing objects using reflection.
+ * <p>
+ * Utility functions in this class do not create instances directly. Instead, they provide reusable functions that can
+ * be used to construct a multitude of instances of the same type. It is advisable to cache this function for reuse.
+ *
+ * @author Mitchell Herrijgers
+ * @since 5.0.0
+ */
+public class ConstructorUtils {
path: messaging/src/main/java/org/axonframework/common/ConstructorUtils.java
user: login: smcvb
body: Thank you! :pray: 
created_at: 2025-03-31T15:14:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,136 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+import jakarta.annotation.Nonnull;
+import org.axonframework.common.ConstructorUtils;
+import org.axonframework.common.annotation.AnnotationUtils;
+import org.axonframework.common.infra.ComponentDescriptor;
+import org.axonframework.eventsourcing.AnnotationBasedEventStateApplier;
+import org.axonframework.eventsourcing.AsyncEventSourcingRepository;
+import org.axonframework.eventsourcing.CriteriaResolver;
+import org.axonframework.eventsourcing.eventstore.AsyncEventStore;
+import org.axonframework.messaging.unitofwork.ProcessingContext;
+import org.axonframework.modelling.repository.AsyncRepository;
+import org.axonframework.modelling.repository.ManagedEntity;
+
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+import static org.axonframework.common.ConstructorUtils.factoryForTypeWithOptionalArgumentInstance;
+
+/**
+ * Implementation of a {@link AsyncRepository} that configures an entity based on the configuration provided by the
+ * {@link EventSourcedEntity} annotation. Please refer to the {@link EventSourcedEntity} annotation for more information
+ * on the configuration options.
+ *
+ * @param <ID> The type of the identifier of the entity.
+ * @param <T>  The type of the entity.
+ */
+public class AnnotationBasedEventSourcingEntityRepository<ID, T> implements AsyncRepository.LifecycleManagement<ID, T> {
path: eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/AnnotationBasedEventSourcingEntityRepository.java
user: login: smcvb
body: Hehe, I am entirely fine that the one merging last will be the one picking up the `EntityBuilder` for this ;-)
At any rate, I agree to keep this repo for now, given that you cannot base yourself on the `EntityBuilder` at this moment in time.
created_at: 2025-03-31T15:15:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+import org.axonframework.eventsourcing.CriteriaResolver;
+import org.axonframework.eventsourcing.EventSourcingRepository;
+import org.axonframework.eventsourcing.eventstore.EventCriteria;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to configure several aspects of an event-sourced entity. This annotation is required to construct a
+ * {@link AnnotationBasedEventSourcingEntityRepository}.
+ * <p>
+ * While loading the entity from the {@link EventSourcingRepository}, the provided {@code id} needs to be translated to
+ * an {@link EventCriteria} instance to load the correct events. Unless overridden, this translation is done by the
+ * {@link AnnotationBasedEventCriteriaResolver}. So, {@link EventCriteria} will be resolved in the following order:
+ * <ol>
+ *     <li>
+ *         Using the custom {@link #criteriaResolver()}. The provided class should implement the {@link CriteriaResolver},
+ *         and have a no-arg constructor, or a 1-arg constructor with the {@link Class} of the entity as parameter.
+ *     </li>
+ *     <li>
+ *         By defining a static method in the entity class annotated with {@link EventCriteriaBuilder} which returns an
+ *         {@link EventCriteria} and accepts the {@code id} as a parameter. This method should be static and return an
+ *         {@link EventCriteria}. Multiple methods can be defined with different id types, and the first matching method
+ *         will be used.
+ *     </li>
+ *     <li>
+ *         If no matching {@link EventCriteriaBuilder} is found, the {@link EventSourcedEntity#tagKey()} will be used as the tag key, and the {@link Object#toString()} of the id will be used as value.
+ *     </li>
+ *     <li>
+ *         If the {@link EventSourcedEntity#tagKey()} is empty, the {@link Class#getSimpleName()} of the entity will be used as tag key, and the {@link Object#toString()} of the id will be used as value.
+ *         Note that the tag format rules are undecided until <a href="https://github.com/AxonFramework/AxonFramework/issues/3326">issue 3326/a> is resolved. This may change in the future.
+ *     </li>
+ * </ol>
+ *
+ * <p>
+ * The {@link #entityCreator()} is used to create a new instance of the entity. The provided class should implement the
+ * {@link EventSourcedEntityFactory} interface, and have a no-arg constructor, or a 1-arg constructor with the {@link Class} of the entity as parameter.
+ * By default, the {@link ConstructorBasedEventSourcedEntityFactory} is used, which creates a new instance using the
+ * no-arg constructor of the entity class, or a 1-arg constructor with the id as parameter.
+ *
+ * @author Mitchell Herrijgers
+ * @see EventSourcingRepository
+ * @see CriteriaResolver
+ * @see AnnotationBasedEventCriteriaResolver
+ * @see EventCriteriaBuilder
+ * @see EventSourcedEntityFactory
+ * @since 5.0.0
+ */
+@SuppressWarnings("unused")
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface EventSourcedEntity {
+
+    /**
+     * The tag name to use when resolving the {@link EventCriteria} for the entity. If empty, the simple name of the
+     * entity class will be used.
+     * <p>
+     * This value does not take effect if a matching {@link EventCriteriaBuilder} is found, or a custom
+     * {@link #criteriaResolver()} is provided.
+     *
+     * @return The tag name to use when resolving the {@link EventCriteria} for the entity.
+     */
+    String tagKey() default "";
+
+    /**
+     * The class to use to resolve the {@link EventCriteria} for the entity. The provided class should implement the
+     * {@link CriteriaResolver} interface, and have a no-arg constructor, or a 1-arg constructor with the {@link Class}
+     * of the entity as parameter.
+     * @return The class to use to resolve the {@link EventCriteria} for the entity.
+     */
+    Class<? extends CriteriaResolver> criteriaResolver() default AnnotationBasedEventCriteriaResolver.class;
+
+    /**
+     * The class to use to create a new instance of the entity. The provided class should implement the
+     * {@link EventSourcedEntityFactory} interface, and have a no-arg constructor, or a 1-arg constructor with the
+     * {@link Class} of the entity as parameter.
+     * @return The class to use to create a new instance of the entity.
+     */
+    Class<? extends EventSourcedEntityFactory> entityCreator() default ConstructorBasedEventSourcedEntityFactory.class;
path: eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/EventSourcedEntity.java
user: login: smcvb
body: I think we should call this the `entityFactory` too if we rename the functional interface.
created_at: 2025-03-31T15:18:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+/**
+ * Functional interface towards creating a new instance of an entity of the given {@code entityType} and the given
+ * {@code id}.
+ *
+ * @since 5.0.0
+ * @author Mitchell Herrijgers
+ * @param <ID> The type of the identifier of the entity to create.
+ * @param <E>  The type of the entity to create.
+ */
+@FunctionalInterface
+public interface EventSourcedEntityFactory<ID, E> {
+
+    /**
+     * Creates a new instance of an entity of the given {@code entityType} and {@code idType}.
+     *
+     * @param entityType The type of the entity to create.
+     * @param id         The identifier of the entity to create.
+     * @return A new instance of the entity.
+     */
+    E createEntity(Class<E> entityType, ID id);
path: eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/EventSourcedEntityFactory.java
user: login: smcvb
body: Nitty Steven is back again: perhaps we do `@Nonnull` to these parameters (and thus also the implementation)? :angel: 
created_at: 2025-03-31T15:18:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+/**
+ * Functional interface towards creating a new instance of an entity of the given {@code entityType} and the given
+ * {@code id}.
+ *
+ * @since 5.0.0
+ * @author Mitchell Herrijgers
+ * @param <ID> The type of the identifier of the entity to create.
+ * @param <E>  The type of the entity to create.
+ */
+@FunctionalInterface
+public interface EventSourcedEntityFactory<ID, E> {
+
+    /**
+     * Creates a new instance of an entity of the given {@code entityType} and {@code idType}.
+     *
+     * @param entityType The type of the entity to create.
+     * @param id         The identifier of the entity to create.
+     * @return A new instance of the entity.
+     */
+    E createEntity(Class<E> entityType, ID id);
path: eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/EventSourcedEntityFactory.java
user: login: CodeDrivenMitch
body: Yes!

created_at: 2025-03-31T15:53:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2010-2025. Axon Framework
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.axonframework.eventsourcing.annotation;
+
+import org.axonframework.eventsourcing.CriteriaResolver;
+import org.axonframework.eventsourcing.EventSourcingRepository;
+import org.axonframework.eventsourcing.eventstore.EventCriteria;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to configure several aspects of an event-sourced entity. This annotation is required to construct a
+ * {@link AnnotationBasedEventSourcingEntityRepository}.
+ * <p>
+ * While loading the entity from the {@link EventSourcingRepository}, the provided {@code id} needs to be translated to
+ * an {@link EventCriteria} instance to load the correct events. Unless overridden, this translation is done by the
+ * {@link AnnotationBasedEventCriteriaResolver}. So, {@link EventCriteria} will be resolved in the following order:
+ * <ol>
+ *     <li>
+ *         Using the custom {@link #criteriaResolver()}. The provided class should implement the {@link CriteriaResolver},
+ *         and have a no-arg constructor, or a 1-arg constructor with the {@link Class} of the entity as parameter.
+ *     </li>
+ *     <li>
+ *         By defining a static method in the entity class annotated with {@link EventCriteriaBuilder} which returns an
+ *         {@link EventCriteria} and accepts the {@code id} as a parameter. This method should be static and return an
+ *         {@link EventCriteria}. Multiple methods can be defined with different id types, and the first matching method
+ *         will be used.
+ *     </li>
+ *     <li>
+ *         If no matching {@link EventCriteriaBuilder} is found, the {@link EventSourcedEntity#tagKey()} will be used as the tag key, and the {@link Object#toString()} of the id will be used as value.
+ *     </li>
+ *     <li>
+ *         If the {@link EventSourcedEntity#tagKey()} is empty, the {@link Class#getSimpleName()} of the entity will be used as tag key, and the {@link Object#toString()} of the id will be used as value.
+ *         Note that the tag format rules are undecided until <a href="https://github.com/AxonFramework/AxonFramework/issues/3326">issue 3326/a> is resolved. This may change in the future.
+ *     </li>
+ * </ol>
+ *
+ * <p>
+ * The {@link #entityCreator()} is used to create a new instance of the entity. The provided class should implement the
+ * {@link EventSourcedEntityFactory} interface, and have a no-arg constructor, or a 1-arg constructor with the {@link Class} of the entity as parameter.
+ * By default, the {@link ConstructorBasedEventSourcedEntityFactory} is used, which creates a new instance using the
+ * no-arg constructor of the entity class, or a 1-arg constructor with the id as parameter.
+ *
+ * @author Mitchell Herrijgers
+ * @see EventSourcingRepository
+ * @see CriteriaResolver
+ * @see AnnotationBasedEventCriteriaResolver
+ * @see EventCriteriaBuilder
+ * @see EventSourcedEntityFactory
+ * @since 5.0.0
+ */
+@SuppressWarnings("unused")
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface EventSourcedEntity {
+
+    /**
+     * The tag name to use when resolving the {@link EventCriteria} for the entity. If empty, the simple name of the
+     * entity class will be used.
+     * <p>
+     * This value does not take effect if a matching {@link EventCriteriaBuilder} is found, or a custom
+     * {@link #criteriaResolver()} is provided.
+     *
+     * @return The tag name to use when resolving the {@link EventCriteria} for the entity.
+     */
+    String tagKey() default "";
+
+    /**
+     * The class to use to resolve the {@link EventCriteria} for the entity. The provided class should implement the
+     * {@link CriteriaResolver} interface, and have a no-arg constructor, or a 1-arg constructor with the {@link Class}
+     * of the entity as parameter.
+     * @return The class to use to resolve the {@link EventCriteria} for the entity.
+     */
+    Class<? extends CriteriaResolver> criteriaResolver() default AnnotationBasedEventCriteriaResolver.class;
+
+    /**
+     * The class to use to create a new instance of the entity. The provided class should implement the
+     * {@link EventSourcedEntityFactory} interface, and have a no-arg constructor, or a 1-arg constructor with the
+     * {@link Class} of the entity as parameter.
+     * @return The class to use to create a new instance of the entity.
+     */
+    Class<? extends EventSourcedEntityFactory> entityCreator() default ConstructorBasedEventSourcedEntityFactory.class;
path: eventsourcing/src/main/java/org/axonframework/eventsourcing/annotation/EventSourcedEntity.java
user: login: CodeDrivenMitch
body: Yep, you are completely right
created_at: 2025-03-31T15:54:23Z
author_association: CONTRIBUTOR

PR comments:
user: login: abuijze
created_at: 2025-03-27T20:48:24Z
body: What would you think of adding annotations on the `@EventSourcingHandler`, either next to it, or as part of a property on the `@EventSourcingHandler` annotation itself.

For example:
```
@EventSourcingHandler
@MatchingTags(
        value = {@MatchingTag(key = "Student", idProperty = "menteeId"),
                @MatchingTag(key = "Student", idProperty = "mentorId")},
        match = MatchingTags.MatchMode.ANY)
public void handle(MentorAssignedToStudentEvent event) {
    if (event.mentorId().equals(this.identifier.mentorId())) {
        mentorHasMentee = true;
    } else if (event.menteeId().equals(this.identifier.menteeId())) {
        menteeHasMentor = true;
    }
}
```
(note that the container annotation is explicitly mentions to allow the `match` attribute to deviate from the default, which is to require a match will all mentioned tags.)

Is the annotation equivalent of:
```
@EventCriteriaBuilder
    public static EventCriteria resolveCriteria(StudentMentorModelIdentifier id) {
        return EventCriteria.either(
                EventCriteria.match()
                             .eventsOfTypes(MentorAssignedToStudentEvent.class.getName())
                             .withTags(new Tag("Student", id.menteeId())),
                EventCriteria.match()
                             .eventsOfTypes(MentorAssignedToStudentEvent.class.getName())
                             .withTags(new Tag("Student", id.mentorId()))
        );
    }
```
author_association: CONTRIBUTOR
user: login: MateuszNaKodach
created_at: 2025-03-28T09:50:13Z
body: > What would you think of adding annotations on the `@EventSourcingHandler`, either next to it, or as part of a property on the `@EventSourcingHandler` annotation itself.
> 
> For example:
> 
> ```
> @EventSourcingHandler
> @MatchingTags(
>         value = {@MatchingTag(key = "Student", idProperty = "menteeId"),
>                 @MatchingTag(key = "Student", idProperty = "mentorId")},
>         match = MatchingTags.MatchMode.ANY)
> public void handle(MentorAssignedToStudentEvent event) {
>     if (event.mentorId().equals(this.identifier.mentorId())) {
>         mentorHasMentee = true;
>     } else if (event.menteeId().equals(this.identifier.menteeId())) {
>         menteeHasMentor = true;
>     }
> }
> ```
> 
> (note that the container annotation is explicitly mentions to allow the `match` attribute to deviate from the default, which is to require a match will all mentioned tags.)
> 
> Is the annotation equivalent of:
> 
> ```
> @EventCriteriaBuilder
>     public static EventCriteria resolveCriteria(StudentMentorModelIdentifier id) {
>         return EventCriteria.either(
>                 EventCriteria.match()
>                              .eventsOfTypes(MentorAssignedToStudentEvent.class.getName())
>                              .withTags(new Tag("Student", id.menteeId())),
>                 EventCriteria.match()
>                              .eventsOfTypes(MentorAssignedToStudentEvent.class.getName())
>                              .withTags(new Tag("Student", id.mentorId()))
>         );
>     }
> ```

Personally, I don't like such complex annotations, but I do agree that it may be an option if someone prefers this style. I also think we don't need it now, especially in this PR, and maybe not even for the milestone. Maybe we should check if anyone complains about the `@EventCriteriaBuilder` after the milestone release?
author_association: CONTRIBUTOR
user: login: CodeDrivenMitch
created_at: 2025-03-28T09:55:29Z
body: @abuijze It's definitely an option.
Personally, I would prefer the `@EventCriteriaBuilder` method, as I would not have to repeat an annotation on every `@EventSourcingHandler`. But, I do admit that the tags that need to be retrieved might vary based on the command itself. 
Right now, the user could specify different ids for different commands, and thus resolve that to different tags.
That's not always ideal.

This makes me think a few things:

- Perhaps we should have both. The annotation on the `EventsourcingHandler` would be an override of the `@EventCriteriaBuilder`
- Perhaps this annotation should not live on the ESH, but on the command handler (maybe as a method reference?)
- Perhaps the `@EventCriteriaBuilder` should be able to take in the CommandMessage as well
author_association: CONTRIBUTOR
user: login: abuijze
created_at: 2025-03-28T11:01:34Z
body: > * Perhaps the `@EventCriteriaBuilder` should be able to take in the CommandMessage as well

In that case, you might as well just support all parameter types using parameter resolvers.

I actually don't think it's a bad idea to allow the CommandMessage to be used as input.


author_association: CONTRIBUTOR
user: login: abuijze
created_at: 2025-03-28T11:02:59Z
body: > Personally, I don't like such complex annotations,

Me neither. But I expect by far the most models to be built up using a single tag anyway.

Other people don't like mixing framework logic in their entities.

There must be a way to satisfy both groups.....
author_association: CONTRIBUTOR
user: login: sonarqubecloud[bot]
created_at: 2025-03-31T16:13:06Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=AxonFramework_AxonFramework&pullRequest=3353) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [10 New issues](https://sonarcloud.io/project/issues?id=AxonFramework_AxonFramework&pullRequest=3353&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=AxonFramework_AxonFramework&pullRequest=3353&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=AxonFramework_AxonFramework&pullRequest=3353&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [85.8% Coverage on New Code](https://sonarcloud.io/component_measures?id=AxonFramework_AxonFramework&pullRequest=3353&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=AxonFramework_AxonFramework&pullRequest=3353&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=AxonFramework_AxonFramework&pullRequest=3353)


author_association: NONE