Polymorphic Entities

A polymorphic entity lets multiple concrete types share a common abstract parent, each with their own command and event sourcing handlers. The concrete type is determined at creation time and does not change.

If the variants differ only by a flag or enum value, a single entity with conditional logic is simpler. Use polymorphism when variants have structurally different commands, state, or behavior.

Defining a polymorphic hierarchy

The abstract parent class lists every concrete subtype via concreteTypes. Axon scans all listed subtypes for command and event handlers automatically. Subtypes do not need any annotation of their own.

Three approaches are supported:

  1. Declarative: no annotations. Provide a factory that creates the correct concrete type from the first event.

  2. Autodetected: annotate with @EventSourcedEntity(concreteTypes = {…​}). Register with EventSourcedEntityModule.autodetected().

  3. Spring Boot: annotate the abstract parent with @EventSourced(concreteTypes = {…​}). Auto-configuration handles registration.

Declarative

In the declarative approach, the entity class needs no framework annotations. Polymorphism is handled by an EventSourcedEntityFactory.fromEventMessage() factory that inspects the first event and creates the correct concrete type:

// Abstract parent, no annotations needed
public abstract class CourseEntity {

    protected String courseId;
    protected String title;
    protected int capacity;
    protected int enrolledCount;
}

// Concrete subtypes
public class OnlineCourse extends CourseEntity {

    protected String platformUrl;

    public OnlineCourse(CourseCreated event) {
        this.courseId = event.courseId();
        this.title = event.title();
        this.capacity = event.capacity();
        this.platformUrl = event.platformUrl();
    }
}

public class InPersonCourse extends CourseEntity {

    protected String location;

    public InPersonCourse(CourseCreated event) {
        this.courseId = event.courseId();
        this.title = event.title();
        this.capacity = event.capacity();
        this.location = event.location();
    }
}

The polymorphism-specific piece is the entityFactory: an EventSourcedEntityFactory.fromEventMessage that inspects the first event and creates the right concrete type. Subtype-specific command handlers cast the entity parameter to that subtype.

Refer to Declarative configuration for the surrounding registration boilerplate: messagingModel((config, model) → { …​ }), criteriaResolver, entityIdResolver, and the basic creationalCommandHandler / instanceCommandHandler / entityEvolver patterns. Only the polymorphism-specific snippets are shown below:

// Subtype-specific instance command handler. Cast to the concrete type.
.instanceCommandHandler(
    resolver.resolveOrThrow(UpdatePlatformUrl.class).qualifiedName(),
    (command, entity, context) -> {
        OnlineCourse online = (OnlineCourse) entity; (1)
        UpdatePlatformUrl cmd = command.payloadAs(UpdatePlatformUrl.class);
        EventAppender.forContext(context).append(
                new PlatformUrlUpdated(online.courseId, cmd.newUrl())
        );
        return MessageStream.empty().cast();
    }
)
// Entity factory: pick the concrete type based on the first event.
.entityFactory(c -> EventSourcedEntityFactory.fromEventMessage((id, firstEvent) -> { (2)
    CourseCreated e = firstEvent.payloadAs(CourseCreated.class);
    return switch (e.courseType()) {
        case ONLINE    -> new OnlineCourse(e);
        case IN_PERSON -> new InPersonCourse(e);
    };
}))
1 Cast the parameter to the concrete subtype to access subtype-specific fields. Safe here because UpdatePlatformUrl can only target an OnlineCourse.
2 fromEventMessage receives the entity identifier and the first event in the stream. Inspect the event to determine which concrete type to instantiate. This replaces the @EntityCreator static factory used in the autodetected approach.

Autodetected

Use @EventSourcedEntity(concreteTypes = {…​}) and register with EventSourcedEntityModule.autodetected(). Axon discovers concrete subtypes from concreteTypes.

If the entity class is a sealed class or interface, concreteTypes is optional. Axon automatically collects all concrete (non-sealed) leaf types by recursively traversing the sealed hierarchy; no explicit listing is required.

import org.axonframework.eventsourcing.annotation.EventSourcedEntity;
import org.axonframework.eventsourcing.annotation.reflection.EntityCreator;

@EventSourcedEntity(tagKey = "courseId", concreteTypes = {OnlineCourse.class, InPersonCourse.class}) (1)
public abstract class CourseEntity {

    // omitted: state, command handlers, event sourcing handlers
    // see xref:commands:entities/event-sourced-entity.adoc[Event-sourced entities] for the structure of these members.

    @EntityCreator
    public static CourseEntity create(CourseCreated event) { (2)
        return switch (event.courseType()) {
            case ONLINE    -> new OnlineCourse(event);
            case IN_PERSON -> new InPersonCourse(event);
        };
    }
}
1 Subtypes do not need @EventSourcedEntity; Axon discovers them from concreteTypes.
2 The @EntityCreator factory picks the concrete subtype based on the creation event. This is the polymorphism-specific part.

Register the abstract parent class:

import org.axonframework.eventsourcing.configuration.EventSourcedEntityModule;
import org.axonframework.eventsourcing.configuration.EventSourcingConfigurer;

static void registerCourseEntity(EventSourcingConfigurer configurer) {
    configurer.registerEntity(
        EventSourcedEntityModule.autodetected(String.class, CourseEntity.class) (1)
    );
}
1 Pass the abstract parent class. Axon discovers the concrete subtypes from concreteTypes automatically.

Spring Boot

In Spring Boot, annotate the abstract parent with @EventSourced(concreteTypes = {…​}) instead of @EventSourcedEntity(concreteTypes = {…​}). Axon’s auto-configuration detects and registers it, including all listed subtypes; no explicit configuration class is required. Do not annotate the subtypes with @EventSourced; they are already discovered, and since @EventSourced is a Spring @Component, annotating them would cause Spring to create unnecessary beans. The entity class structure is otherwise identical to the autodetected approach.

If the entity class is a sealed class or interface, concreteTypes is optional. Axon automatically collects all concrete (non-sealed) leaf types by recursively traversing the sealed hierarchy; no explicit listing is required.

Constraints

  • For non-sealed entity classes, subtypes not listed in concreteTypes are silently ignored; their command and event handlers will not be registered and commands targeting them will fail at runtime. For sealed entity classes, all concrete (non-sealed) leaf types in the sealed hierarchy are discovered automatically; no explicit listing in concreteTypes is needed.

  • Each command may have at most one handler anywhere in the hierarchy. Axon throws DuplicateCommandHandlerSubscriptionException at startup if the same command appears in more than one class within the same entity type. This applies to both instance and creational command handlers.

  • @EventSourcingHandler methods can be declared on both the parent and subtypes; the parent’s handler is called first, then the subtype’s.

  • Parent fields read or assigned by subtype constructors must be at least protected; private fields cannot be accessed from subclasses.

  • The concrete type is fixed at creation time; an entity cannot change its type at runtime.