Aggregate Class Hierarchies and Heterogeneous Aggregate APIs

Hello,

I am currently working on a domain model, where the ubiquitous language basically calls for different kinds of the same upper class. It is a domain I know well for years now. Not only do these different types share a set of basic commands, but they are also referenced by other aggregates in the same generic way.

The problem I run into:

  • Test is superclass

  • Test1/Test2 Subclasses

  • Constructor commands work fine

  • The commands declared by Test work fine

  • The commands declared to be handled by Test1/Test2 fail in command handling

The stacktrace below is created when creating a Test1 and dispatching a ModifyTest1Command.

My current very ugly workaround: Make Test non-abstract and an @Aggregate. Declare all CommandHandlers for all sub-types in Test, let them throw an Exception when called there. @Override the appropriate command handler in Type1/Type2 to add domain logic. So I ened up with a very long list of these dummy command and event handlers in my top level class. Makes maintenance a mess as well.

Any idea on how to resolve this cleanly that the example below works ? Or am I doing something wrong?

Thanks everybody.

Dominic

I made up a small toy example of this (using lombok and kotlin):

API in .kt:
`

class CreateTest1Command(val testId: String)
class CreateTest2Command(val testId: String)

class ModifyTestCommand(@TargetAggregateIdentifier val testId: String)
class ModifyTest1Command(@TargetAggregateIdentifier val testId: String)
class ModifyTest2Command(@TargetAggregateIdentifier val testId: String)

class Test1CreatedEvent(@TargetAggregateIdentifier val testId: String)
class Test2CreatedEvent(@TargetAggregateIdentifier val testId: String)

`

`
@Slf4j
public abstract class Test {

@Getter
@Setter
@AggregateIdentifier
protected String testId;

@CommandHandler
public void handle(ModifyTestCommand command) {
log.info(“handle by {}: {}”, this.getClass().getSimpleName(), command);
}
}

`

`
@Slf4j
@NoArgsConstructor
@Aggregate(repository = “testRepository”)
public class Test1 extends Test {

@CommandHandler
public Test1(CreateTest1Command command) {
log.info(“1-handle: {}”, command);
apply(new Test1CreatedEvent(command.getTestId()));
}

@CommandHandler
public void handle(ModifyTest1Command command) {
log.info(“1-handle: {}”, command);
}

@EventSourcingHandler
public void on(Test1CreatedEvent event) {
setTestId(event.getTestId());
}
}

`

`
@Slf4j
@NoArgsConstructor
@Aggregate(repository = “testRepository”)
public class Test2 extends Test {

@CommandHandler
public Test2(CreateTest2Command command) {
log.info(“2-handle: {}”, command);
apply(new Test2CreatedEvent(command.getTestId()));
}

@CommandHandler
public void handle(ModifyTest2Command command) {
log.info(“2-handle: {}”, command);
}

@EventSourcingHandler
public void on(Test2CreatedEvent event) {
setTestId(event.getTestId());
}
}

`

`

@Configuration
public class TestConfiguration {

@Autowired
EventStore eventStore;

@Bean
public Repository testRepository() {
return new EventSourcingRepository<>(new TestAggregateFactory(), eventStore);
}
}

@Slf4j
public class TestAggregateFactory implements AggregateFactory {

@Override
public Test createAggregateRoot(String aggregateIdentifier, DomainEventMessage<?> firstEvent) {
if (firstEvent.getPayloadType().equals(Test1CreatedEvent.class)) {
log.info(“Instantiate -> Test1”);
return new Test1();
} else if (firstEvent.getPayloadType().equals(Test2CreatedEvent.class)) {
log.info(“Instantiate -> Test2”);
return new Test2();
} else {
throw new IllegalArgumentException(
"this aggregate factory does not support " + firstEvent.getPayloadType().getSimpleName());
}
}

@Override
public Class getAggregateType() {
return Test.class;
}

}

`

`

Command resulted in exception: org.openconjurer.directory.command.agent.ModifyTest1Command

java.lang.NullPointerException: null
at org.axonframework.commandhandling.model.inspection.AnnotatedAggregate.lambda$handle$3(AnnotatedAggregate.java:194) ~[axon-core-3.0-M5.jar:3.0-M5]
at org.axonframework.commandhandling.model.AggregateLifecycle.executeWithResult(AggregateLifecycle.java:75) ~[axon-core-3.0-M5.jar:3.0-M5]
at org.axonframework.commandhandling.model.inspection.AnnotatedAggregate.handle(AnnotatedAggregate.java:192) ~[axon-core-3.0-M5.jar:3.0-M5]
at org.axonframework.commandhandling.model.LockAwareAggregate.handle(LockAwareAggregate.java:60) ~[axon-core-3.0-M5.jar:3.0-M5]
at org.axonframework.commandhandling.AggregateAnnotationCommandHandler$AggregateCommandHandler.handle(AggregateAnnotationCommandHandler.java:192) ~[axon-core-3.0-M5.jar:3.0-M5]
at org.axonframework.commandhandling.AggregateAnnotationCommandHandler$AggregateCommandHandler.handle(AggregateAnnotationCommandHandler.java:186) ~[axon-core-3.0-M5.jar:3.0-M5]
at org.axonframework.commandhandling.AggregateAnnotationCommandHandler.handle(AggregateAnnotationCommandHandler.java:148) ~[axon-core-3.0-M5.jar:3.0-M5]
at org.axonframework.commandhandling.AggregateAnnotationCommandHandler.handle(AggregateAnnotationCommandHandler.java:40) ~[axon-core-3.0-M5.jar:3.0-M5]
at org.axonframework.messaging.DefaultInterceptorChain.proceed(DefaultInterceptorChain.java:57) ~[axon-core-3.0-M5.jar:3.0-M5]
at org.axonframework.messaging.interceptors.CorrelationDataInterceptor.handle(CorrelationDataInterceptor.java:46) ~[axon-core-3.0-M5.jar:3.0-M5]
at org.axonframework.messaging.DefaultInterceptorChain.proceed(DefaultInterceptorChain.java:55) ~[axon-core-3.0-M5.jar:3.0-M5]
at org.axonframework.messaging.unitofwork.DefaultUnitOfWork.executeWithResult(DefaultUnitOfWork.java:66) ~[axon-core-3.0-M5.jar:3.0-M5]
at org.axonframework.commandhandling.SimpleCommandBus.doDispatch(SimpleCommandBus.java:143) [axon-core-3.0-M5.jar:3.0-M5]
at org.axonframework.commandhandling.SimpleCommandBus.doDispatch(SimpleCommandBus.java:119) [axon-core-3.0-M5.jar:3.0-M5]
at org.axonframework.commandhandling.SimpleCommandBus.dispatch(SimpleCommandBus.java:89) [axon-core-3.0-M5.jar:3.0-M5]
at org.axonframework.commandhandling.CommandBus.dispatch(CommandBus.java:45) [axon-core-3.0-M5.jar:3.0-M5]
`

Hi,

this structure isn’t currently supported. Axon, at the moment, only supports aggregates where the @CommandHandler annotated methods are in the superclass. You can still have multiple implementations of the (abstract) super type.
Do not annotate your subclasses with @Aggregate.

The @CommandHandler methods that create the various implementations of the Test aggregate will need to be defined on a CommandHandler bean which creates an appropriate instance with the TestRepository.

This is something that we plan to address is future versions (e.g. 3.1) of Axon.

Cheers,

Allard

Hello, Allard.

Thanks for the clarification. That is basically what I am doing right now. However, AFAIK in the beginning I got errors that I may not use abstract classes.
So currently the root class has tons of methods of this form:

`
@CommandHandler
public void handle(DoSomethingCommand command) {
throw new UnsupportedOperationException(UNSUPPORTED_COMMAND + this.getClass().getSimpleName());
}

`

The example I sketched here worked perfectly fine with regards to the constructor commands. Also the shared command was routed perfectly fine to the correct instance/type. Supporting this seems to be primarily an issue of adjusting the reflection algotithm looking for the sub-class command handlers.

Making the handler methods abstract also is super annoying, because this means if I have sub-class A implement C1, C2 and sub-class B implement C3, C4, I have to provide a dummy implementation of C3,C4 in A and dummy implementations of C1,C2 in B. So currently I implement C1,C2,C3,C4 in the parent class and throw the UnsupportedOperationException if they are called. At least this way A and B are clean from domain specific methods of the other sub-type.

So I will have to keep it this way and at least I know that it currently is the right way to do it.

Thanks.

Dominic