Entity Identifier Resolution

Before invoking a command handler on an entity, Axon resolves the identifier of the entity instance the command targets. An EntityIdResolver performs this step: given a message, it returns the entity identifier.

Entity identifier resolution is separate from command routing. The routing key (see Command routing) selects the application instance that receives a command. The EntityIdResolver selects the entity instance that application instance loads.

The EntityIdResolver

EntityIdResolver<ID> is a functional interface with a single method:

ID resolve(Message message, ProcessingContext context) throws EntityIdResolutionException;

The returned identifier is used to load the entity from the StateManager. The identifier can be of any type, as long as it matches the identifier type of the entity. When no identifier can be resolved, the resolver throws an EntityIdResolutionException. The command then fails without invoking a handler.

Axon uses an EntityIdResolver for:

Built-in resolvers

Resolver Resolves the identifier from

AnnotationBasedEntityIdResolver

The payload member (field or method) annotated with @TargetEntityId. This is the default.

PropertyBasedEntityIdResolver

A named payload property. Used for the idProperty attribute of @InjectEntity.

MetadataEntityIdResolver

An entry in the message metadata.

FallbackEntityIdResolver

A primary resolver, with a secondary resolver as fallback.

Annotation-based resolution

The AnnotationBasedEntityIdResolver reads the payload members annotated with @TargetEntityId, as described in Entity identification. Several members may carry the annotation, as long as they result in exactly one distinct, non-null value. Otherwise, it throws an EntityIdResolutionException.

Property-based resolution

The PropertyBasedEntityIdResolver reads the payload field named by its property argument, or the matching method (get<Property> or <Property>). It throws an EntityIdResolutionException when the value is null. Setting @InjectEntity(idProperty = "courseId") constructs this resolver.

Metadata-based resolution

The MetadataEntityIdResolver reads the identifier from the message metadata. Use it when the payload does not carry the entity identifier, for example when the message originates from another model and the target identifier travels as metadata.

EntityIdResolver<String> metadataResolver() {
    return MetadataEntityIdResolver.forKey("courseId"); (1)
}
1 Resolves the identifier from the courseId metadata entry. An absent or blank entry results in an EntityIdResolutionException.

Metadata values are String instances, so forKey(String) resolves a String identifier. For any other identifier type, pass the type and a Converter:

EntityIdResolver<UUID> convertingMetadataResolver(Converter converter) {
    return MetadataEntityIdResolver.forKey("courseId", UUID.class, converter); (1)
}
1 Converts the courseId metadata value to a UUID.

Fallback resolution

The FallbackEntityIdResolver combines two resolvers. It invokes the secondary resolver only when the primary resolver throws an EntityIdResolutionException. Other exceptions of the primary resolver propagate.

EntityIdResolver<String> fallbackResolver() {
    return new FallbackEntityIdResolver<>(
            new AnnotationBasedEntityIdResolver<>(), (1)
            MetadataEntityIdResolver.forKey("courseId") (2)
    );
}
1 The primary resolver reads the @TargetEntityId member of the payload.
2 The secondary resolver reads the courseId metadata entry.

A FallbackEntityIdResolver is an EntityIdResolver itself, so fallbacks can be nested.

Custom resolvers

Implement EntityIdResolver when no built-in resolver fits, for example when the identifier is composed of several fields.

public class FacultyCourseIdResolver implements EntityIdResolver<String> {

    @Override
    public String resolve(Message message, ProcessingContext context) throws EntityIdResolutionException {
        if (!(message.payload() instanceof EnrollInCourse command)) { (1)
            throw new EntityIdResolutionException(
                    "Unable to resolve a course identifier for message [" + message.type() + "]."
            ); (2)
        }
        return command.facultyId() + "/" + command.courseId(); (3)
    }

    public record EnrollInCourse(String facultyId, String courseId, String studentId) {

    }
}
1 Checks whether the payload is of a type this resolver supports.
2 Throws an EntityIdResolutionException when no identifier can be resolved. A FallbackEntityIdResolver relies on this exception to select its secondary resolver.
3 Composes the identifier from two payload fields.

Configuring the resolver of an entity

The way to set a resolver depends on how the entity is defined:

Entity definition Resolver configuration

@InjectEntity parameter

The idProperty or idResolver attribute, see Stateful command handlers. The idResolver attribute takes the Class of an EntityIdResolver. Without either attribute, the AnnotationBasedEntityIdResolver is used.

Declarative entity

The entityIdResolver step of the EventSourcedEntityModule or StateBasedEntityModule, see Declarative.

Annotated entity (@EventSourcedEntity or @EventSourced)

The entityIdResolverDefinition attribute, see The EntityIdResolverDefinition.

The EntityIdResolverDefinition

An annotated entity does not construct its EntityIdResolver itself. Axon asks an EntityIdResolverDefinition to create it. The definition receives the entity type, the identifier type, the entity metamodel, and the application Configuration:

public class MetadataFallbackEntityIdResolverDefinition implements EntityIdResolverDefinition {

    @Override
    public <E, ID> EntityIdResolver<ID> createIdResolver(Class<E> entityType,
                                                         Class<ID> idType,
                                                         EntityMetamodel<E> entityMetamodel,
                                                         Configuration configuration) {
        return new FallbackEntityIdResolver<>( (1)
                new AnnotationBasedEntityIdResolver<>(),
                MetadataEntityIdResolver.forKey("entityId", idType, configuration.getComponent(Converter.class))
        );
    }
}
1 Creates a resolver that reads the @TargetEntityId payload member first, and the entityId metadata entry second. The Configuration provides the Converter required to convert the metadata value to the identifier type.

By default, an annotated entity uses a definition that creates the AnnotationBasedEntityIdResolver. Set the entityIdResolverDefinition attribute to use another definition for a single entity:

@EventSourcedEntity(
        tagKey = "courseId",
        entityIdResolverDefinition = MetadataFallbackEntityIdResolverDefinition.class (1)
)
public class CourseWithExplicitDefinition {
    // Entity state and handlers omitted.
}
1 The definition class requires a public no-argument constructor.

Application-wide definition

Register an EntityIdResolverDefinition as a component to change the default for all annotated entities in the application. Entities that name their own definition on the annotation keep it.

  • Spring Boot

  • Plain Java

@Configuration
public class SpringApplicationWideDefinitionConfiguration {

    @Bean
    public EntityIdResolverDefinition entityIdResolverDefinition() {
        return new MetadataFallbackEntityIdResolverDefinition(); (1)
    }
}
1 Applies to every annotated entity that does not set entityIdResolverDefinition.
public static EventSourcingConfigurer configure(EventSourcingConfigurer configurer) {
    return configurer.componentRegistry(registry -> registry.registerComponent(
            EntityIdResolverDefinition.class, (1)
            config -> new MetadataFallbackEntityIdResolverDefinition()
    ));
}
1 Applies to every annotated entity that does not set entityIdResolverDefinition.

Axon selects the definition of an annotated entity in this order:

  1. The definition set on the entityIdResolverDefinition attribute of the annotation.

  2. The EntityIdResolverDefinition registered as a component.

  3. The default EntityIdResolverDefinition that creates the AnnotationBasedEntityIdResolver.

The application-wide definition applies to annotated entities only. Declarative entities set their resolver explicitly, and @InjectEntity uses its own idProperty and idResolver attributes.

Resolution failures

An EntityIdResolutionException indicates one of the following:

  • The payload has no @TargetEntityId value, or more than one distinct value.

  • The metadata entry read by a MetadataEntityIdResolver is absent or blank.

  • A custom resolver could not determine an identifier.