Aggregate Polymorphism and Snapshotting - how to configure it correctly?

Hello, we have been using the Axon Framework in our product for several years now. In order to implement a new feature, we have decided to utilize the aggregate polymorphism design concept, as we wish to reuse certain parts of the business logic across two aggregates of a similar type. Generally speaking, it has been functioning well. However, we have encountered two issues when introducing snapshotting.

To replicate these issues, we cloned the following GitHub project: GitHub - idugalic/axon-polymorphism-demo: Axon and OOP (Inheritance, Polymorphism).

We added a file named AxonConfiguration.java and configured a default snapshot trigger, setting the threshold to 1 for testing purposes.

@Configuration
public class AxonConfiguration {
    @Bean
    public SnapshotTriggerDefinition defaultSnapshotTrigger(Snapshotter snapshotter) {
        return new EventCountSnapshotTriggerDefinition(snapshotter, 1);
    }
}

Subsequently, we included the snapshotTriggerDefinition as a parameter in the @Aggregate annotation of the abstract class Bird. Although we couldn’t find any relevant information in the documentation, we believed this to be the best location for it.

@Aggregate(snapshotTriggerDefinition = "defaultSnapshotTrigger")
@NoArgsConstructor(access = AccessLevel.PACKAGE)
abstract class Bird {
    ...

We ran AxonServer in a Docker container and implemented a simple REST controller to send commands. After launching the application, we successfully sent the HatchEagleCommand command but still received an error message in the logs:

o.a.eventsourcing.AbstractSnapshotter: An attempt to create and store a snapshot resulted in an exception. Exception summary: Aggregate Type is unknown in this snapshotter: pro.idugalic.axonpolymorphism.command.Bird

While debugging the code, we discovered that the aggregateFactory in the getAggregateFactory method (in org.axonframework.spring.eventsourcing.SpringAggregateSnapshotter) was null. This led to the aforementioned error being logged by org.axonframework.eventsourcing.AggregateSnapshotter. We are concerned that this error may result in undefined behavior. We are currently using axon-spring-boot-starter version 4.7.3.

The second issue we encountered relates to the aggregateType of a snapshot. After sending the FlyCommand twice, we noticed that AxonServer had created a snapshot, with the aggregateType listed as ‘Eagle’. However, when we renamed the Eagle aggregate to ‘XEagle’ and sent the hatching and flying commands again, the aggregateType in the new snapshot was ‘Penguin’, which was unexpected. Upon debugging into the repository, we found that the subtypes of the aggregateFactory were correct, but the aggregateType seemed to be the first one listed in the subtypes.

We attempted to manually configure the aggregates to address the issues we encountered, but unfortunately this did not resolve them.

We would greatly appreciate any guidance or assistance you can offer in resolving these issues. Thank you in advance for your help.

Best regards,
Christiane

Hi @Christiane, and welcome to the forum!

First off, great to hear you and the team have been using Axon Framework for several years now.
I do hope the experience is pleasant. If not, feel free to provide suggestions on how we can improve things.

Now, to the subject matter.
As you’ve found out, there is no dedicated documentation on snapshot configuration for a polymorphic aggregate. On top of that, we do not have any direct test cases for the scenario either.
Due to this, your predicament necessitates the introduction of such a test case, obviously.

Furthermore, you’ve pointed out you’ve cloned a repository from idugalic to validate the predicament.
Could you perhaps share this fork with us here? Or, share a different repository containing a reproducible scenario for the described issues?

That should form a nice start for test case introduction to the Framework repository.
With that in place, figuring out what’s missing and causing the described issues should be straightforward.

Hi @Steven_van_Beelen,

Thank you for dedicating your time to investigate the issues further. I hope it is ok to share the reproducible scenario in this way: axon-polymorphism-demo-fork

We are looking forward to your advice,
Christiane

1 Like

After quite some debugging through Axon’s Spring logic, I have noticed that for some reason the root type Bird is not found as an @Aggregate annotated bean by Springs ConfigurableListableBeanFactory.

Furthermore, after some digging in Axon Framework, I did find test cases validating the SpringPrototypeAggregateFactory to be constructed for polymorphic aggregates.

The predicament I’m facing right now, is that the test case works fine, while your project does not.
In different terms, the Axon Framework test case discovers the @Aggregate annotated root type, while the shared repository does not.

I’ll keep digging more. Stay tuned.

After digging through Spring auto-configuration logic, I found the difference causing the discrepancy in our test case and an actual environment.

Our test case used inner classes for the @Aggregate annotated polymorphic aggregate classes.
These inner classes were part of an @Configuration annotated class.
This @Configuration annotated class can then be selected for the right tests.

It so happens to be that beans maintained in an @Configuration annotated class do not filter out abstract @Component annotated classes.
This filtering is in place whenever component scanning occurs on the classpath, which is the case in your example.

Of course, this merits adjustment to our polymorphic aggregate auto-configuration test case, which I have available locally.

To support snapshotting on polymorphic aggregates in a Spring environment, I have created this issue with Axon Framework.
Note that although it’s slated for 4.8.0 (the upcoming minor release), the priority is set to a Could.

I’ve taken that decision, as there’s a workaround, by setting the AggregateFactory and optionally the Snapshotter manually to the none-Spring specific versions.
Another solution may be to replace just the Snapshotter and override the getAggregateFactory(Class<?>) method.

I hope to have given you sufficient information for now, @Christiane!
If you have any follow up questions be sure to post them.

1 Like

Thank you very much for your debugging efforts. We will examine the suggested workarounds.

Kind regards,
Christiane

1 Like

Hi @Steven_van_Beelen

We attempted to implement the workarounds you suggested, but unfortunately, we were unsuccessful.

DefaultConfigurer.defaultConfiguration()
                .configureAggregate(AggregateConfigurer.defaultConfiguration(Eagle.class)
                        .configureSnapshotTrigger(config -> new EventCountSnapshotTriggerDefinition(
                                config.snapshotter(), 1
                        )))
...

We explored the option of configuring the aggregate as seen above. But this would mean to also configure all other aggregates like this, right? Additionally, we utilize a variety of custom settings that would need to be reconfigured as well.

We also tried to overwrite the snapshotter, but found that simply replacing the getAggregateFactory method was insufficient as we were unable to determine the actual type of the aggregate at that point. Instead, we attempted to overwrite the createSnapshot method. However, this approach led to exceptions (“Unable to resolve parameter”) probably because we inject additional components into the command handlers.

If possible, we would greatly appreciate it if you could provide us with a code snippet for one of the workarounds.

Thank you very much,
Christiane

The AggregateFactory customization should only be necessary for the polymorphic aggregate, not for your other aggregate instances.
Since the Snapshotter is used by all, that would indeed be a more generic configuration.

For the Snapshotter, you should be able to provide a AggregateSnapshotter to your Spring Application Context through a bean creation method. The most important bit there is to register the AggregateFactory for every (root) aggregate type you have. This should hold, as all the SpringAggregateSnapshotter does is override the getAggregateFactory to use the Application Context to find the factories.


New possible workaround

Now that I’m typing the above, I have a hunch you may be able to follow an easier workaround.
You may be able to construct your own SpringPrototypeAggregateFactory implementation that set the outcome of getAggregateType to the root aggregate type. By adding this custom implementation to the Application Context, the SpringAggregateSnapshotter should be able to find the custom bean automatically.

My only doubt with the above is the bean ordering, which may lead the SpringAggregateSnapshotter to first discover the faulty polymorphic SpringPrototypeAggregateFactory instead of the custom version.

Please give this a try and let me know whether it works. As it stands, I don’t have time soon to make a fully functioning workaround for you, @Christiane. I simply have a bit too much on my plate. As stated earlier, though, the fix is slated for 4.8.0, which we aim to release start of June. If the above doesn’t work out, know that you likely by June have a solution anyhow.

@Christiane, I wanted to notify you that I’ve gone ahead and constructed a pull request to fix the snapshotting for polymorphic aggregates.
As it stands, it’ll be a part of the 4.8.0 Axon Framework release, which should be out soon.
If you’re curious, you can find the pull request here.

1 Like

@Steven_van_Beelen We’re really glad to hear that the fix will be included in the next version! That’s great news!

1 Like