Axon Framework 5 suggestions

Hi all,

We as framework team have started to plan features for Axon Framework 5. Since this would be a breaking release, almost anything is possible. We would love to hear from you what you think can be improved, or new features worth considering to be part of a new major release.

5 Likes

Hi folks,

Great news! The first thing that comes in mind is an API redesign of the Query API.

Currently, the query API is very difficult to “communicate”. As a client you need to know the query type, the response type and the multiplicity (response type). In the same time, there is no easy type safe way (like a Java interface) to enforce or check this, since this must be communicated over the system boundary.

I don’t have a nice solution for this, but I feel that this is a problem in every project where Axon Framework is used.

The second thing is the way how the artifacts are cut in the framework. Jan Galinski and I spoke about this with Steven and even in the Podcast. The artifacts should be usage-oriented (query client, command client, command model, query model) and not feature-oriented (es, messaging, etc).

What do you think?

Cheers

Simon

3 Likes

Hi there,

i’ve looked through already open issues and found these, that I find interesting:

At the end I just want to throw in another thought: I’m not a big fan of breaking changes. For me (as a developer) this had always been the absolute last resort. Of course, if there are valuable future features that will be made possible with a thought-through breaking change and there is a migration guide, then it makes sense. I hope, that there won’t be breaking changes only because semantic versioning is seen that way :slight_smile:

Kind regards
Johannes

I think the following breaking changes would definitely be welcome:

And there some other nice to haves which would break the default behaviour:

2 Likes

Would love to see a command response include metadata about the event(s) that were created as a result of that command, so any following queries can include those details so the query handler implementation can block until those events have been handled in the projection

3 Likes

A way to customize the event retry scheme - it would be cool if the ErrorHandler could tell how many times a failed event is retried.

1 Like

Please implement a way to prioritize event handling processors! If priority for processor A is higher than processor B, first handle events for A.

1 Like

This could be done now instead of waiting for Axon5, and wouldn’t be a breaking change.

@Aggregate annotation enhancement to allow a method name to be specified to use for the aggregateId as opposed to annotating a field or method. This is really a convenience thing more than a problem that needs to be solved.

Add a distributed query bus for non-axonserver modes (issue #613)

Hi all,

Past weeks have been a bit hectic, being on vacation, came back with covid, and needing to prepare for a talk. So sorry for the late response.

Thanks a lot for all the suggestions. More are always welcome, some of them were already on our own list, it’s nice to see them validated. I’m a bit scared to make promises, but it’s almost certain we move to Java 17, and might use some a the enhancement to Java to improve the API.

There are also some nice non breaking ideas in here we might pick up later (or even earlier if time allows). As it’s open source please let us know if you would like to work on some of the things suggested. Also feel free to open a new topic if you would like to discuss some ideas in more detail.

Communicating across multiple contexts in Axon is currently unintuitive. I have struggled to find anything in the reference guide and the offered APIs are not very explicit. Since communication between multiple bounded contexts is a central concept in DDD I feel like it should be supported better.

I created two threads (Thread 1, Thread 2) on this issue and the answers helped me solve my problem. I added some thoughts on how I would imagine the API. Maybe it has some place in Axon 5. Anyways, I hope I can spark a conversation in your team on this topic.

PS: Any best practices regarding communication between contexts in Axon are more than welcome. I am still experimenting with this and I am looking for design guidelines.

1 Like

Hi @Gerard, if moving to Java17 is on the cards would this mean a move to support Springboot?

We are already supporting Springboot, but there might be things we can improve?

Sorry @Gerard, that was supposed to say Springboot3

Yes, we plan to move to Springboot 3.

1 Like

I have three suggestions:

In order to make use of ACID transations and avoid dual writes it might be beneficial to support something like the Outbox pattern.

I already started some discussions some years ago:


Somehow related, but with another focus:
We would like to have a native option to decide if a certain event should be exposed to the outside world or should only be visible and usable within a certain module / bounded context.

@Simon_Zambrovski, @Steven_van_Beelen and I already discussed this topic in the Contributions section. Although there has not been any action in the last couple of months the topic is still relevant for us. (For those who are allowed to see the thread: https://discuss.axoniq.io/t/local-event-store-support/2980)


From a conceptual point of view the append-only Event Store is a great idea. Unfortunately in our projects we experienced that this leads to problems for us. Sometimes it’s not feasable to extend the upcaster chain in order to evolve our events. In the context of GDPR we sometimes wish there was a solution to anonymize data in old events.

I know that “changing” the event store and therefore the history comes with its own flaws, but I just would like to share that having an append-only event store sometimes leads to cumbersome solutions and rewriting history in certain cases is much more pragmatic.


That being said we highly appreciate your work and there are a lot of ongoing improvements in the 4.x stream.
Especially the “Dead letter queue” is something that I would have requested for 5.x if it would not be planned for 4.x.

4 Likes

I’ve run into an issue today where an enahncement to the injection of resources into event handlers would be helpful. It would be really helpful to have the ability to inject attributes / fields from an event message into the event handler and have this taken into account when choosing which event handler to invoke. Something like the following:-

class MyEvent {
    String attribute1;
    String attribute2;
}

class AggregateClass {
    ...
    @EventSourcingHandler(payloadType = MyEvent.class) 
    void on(@EventAttribute(required = false) String attribute1) {
        ...
    }

    @EventSourcingHandler(payloadType = MyEvent.class)
    void on(@EventAttribute(required = true) String attribute2) {
        ...
    }
}

In this instance I’ve made up an @EventAttribute annotation but it could be anything that makes the most sense, possibly @PayloadAttribute or @MessageAttribute

2 Likes

In a similar vein to the previous suggestion, another nice feature would be the ability to inject resources into aggregates at the time of creation, instead of having to pass resources to @ComandHandler and @EventSourcingHandler methods. This would work the same as it does for sagas, e.g.

@Component
class MyResource {
    ...
}

@Aggregate
class AggregateClass {
    ...
    private transient MyResource myResource;

    @Autowired
    void setMyResource(MyResource myResource) {
        this.myResource = myResource;
    }
    ...
}

In this way, if the resource is used in multiple command / event handlers, it only needs to be injected once as opposed to on every method that requires it.

2 Likes

Hello @andye, I actually really like the suggestion. I do want to note that this is currently already possible! You can write this yourself and register the ParameterResolverFactory.

If we define the following annotation:

@Documented
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface PayloadAttribute {
    String path();

    boolean required() default false;
}

We can then create the following factory, assuming you use Jackson:

public class PayloadAttributeParameterResolverFactory implements ParameterResolverFactory {
    @Override
    public ParameterResolver createInstance(final Executable executable, final Parameter[] parameters, final int i) {
        if (AnnotationUtils.isAnnotationPresent(parameters[i], PayloadAttribute.class)) {
            final Map<String, Object> propertyMap = AnnotationUtils.findAnnotationAttributes(parameters[i], PayloadAttribute.class).get();
            return new PayloadAttributeParameterResolver((String) propertyMap.get("path"), (Boolean) propertyMap.get("required"));
        }
        return null;
    }

    private static class PayloadAttributeParameterResolver implements ParameterResolver<Object> {
        private final ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules();
        private final String path;
        private final boolean required;

        PayloadAttributeParameterResolver(String path, boolean required) {
            this.path = path;
            this.required = required;
        }

        @Override
        public Object resolveParameterValue(final Message<?> message) {
            JsonNode currentNode = objectMapper.valueToTree(message.getPayload());

            for (String part : path.split("\\.")) {
                if (!currentNode.isObject() || !currentNode.has(part) || currentNode.isNull()) {
                    currentNode = null;
                    break;
                }
                currentNode = currentNode.get(part);
            }

            if (currentNode == null || currentNode.isNull()) {
                return null;
            }

            if (currentNode.isTextual()) {
                return currentNode.textValue();
            }
            if (currentNode.isDouble()) {
                return currentNode.doubleValue();
            }
            if (currentNode.isLong()) {
                return currentNode.longValue();
            }
            if(currentNode.isInt()) {
                return currentNode.intValue();
            }

            return null;
        }

        @Override
        public boolean matches(final Message<?> message) {
            if (!required) {
                return true;
            }
            return resolveParameterValue(message) != null;
        }
    }
}

This will provide the functionality to use multiple methods for the same event based on payload, for example:

@Aggregate
class Account {

    private Account() {
    }

    @CommandHandler
    Account(RegisterAccountCommand command, @PayloadAttribute(path = "password", required = true) String password) {
        apply(new AccountRegisteredEvent(command.getAccountId(), command.getUserName(), command.getPassword()));
    }

    @CommandHandler
    Account(RegisterAccountCommand command) {
        apply(new AccountRegisteredEvent(command.getAccountId(), command.getUserName(), "nopassword"));
    }
}

This sample uses the annotation to deviate between a command with and without a password, but you can use it any way you like!

1 Like