AxonFramework icon indicating copy to clipboard operation
AxonFramework copied to clipboard

Improved API for Aggregate State Transitions

Open vab2048 opened this issue 5 years ago • 5 comments

Feature Request: Improved API for state transitions.

I came across a wonderful project by @idugalic on github (https://github.com/idugalic/axon-statemachine-demo) which highlights the new polymorphic aggregate feature of axon as well as a finite state machine for an illustrative Order aggregate.

The github README describes the transitions which are valid/invalid for the aggregate and there is also a good image which I am copying here for easier reference:

state-machine

So, we can transition from an order created to an order paid or order cancelled state. Below is an example of how state transitions are currently expressed in code:

From OrderCreated -> OrderPaid:

@Override @CommandHandler
void on(MarkOrderAsPaidCommand command) {
    apply(new OrderPayingInitiatedEvent(command.getAggregateIdentifier())).andThen(() -> {
        try { createNew(OrderPaid.class, () -> new OrderPaid(orderId, OrderStatus.NEW, items)); }
        catch (Exception e) {
            log.error("Can't create OrderPaid aggregate", e);
            throw new CommandExecutionException("Can't transition to PAID state", e);
        }
    });
}

From OrderCreated -> OrderCancelled:

@Override @CommandHandler
void on(MarkOrderAsCancelledCommand command) {
    apply(new OrderCancellationInitiatedEvent(command.getAggregateIdentifier())).andThen(() -> {
        try { createNew(OrderCanceled.class, () -> new OrderCanceled(orderId, OrderStatus.NEW, items)); } 
       catch (Exception e) {
            log.error("Can not create OrderCanceled aggregate", e);
            throw new CommandExecutionException("Can't transition to CANCELED state", e);
        }
    });
}

My feature request is to improve the API so that the code can be read directly to express the fact that these are transitions. Currently the usage of createNew kind of throws me because it means we are creating a new aggregate (when in my head a transition would still mean it is the same aggregate). Perhaps instead of createNew there could be a stateTransition method, or perhaps there could be an annotated method @StateTransition.

Perhaps my reading of the code is wrong - please chime in if I have misread something.

I don't have a concrete proposal for API improvement just yet but I'm creating this issue for discussions around how an "improved" API for state transitions would look within Axon. I may post back with further discussion points once I have some time.

Regards, vab2048

vab2048 avatar Apr 21 '20 12:04 vab2048

Thanks for filing this with us @vab2048. As it stands, we were already having discussion between the team members to introduce such a feature, with some thoughts on the API as well.

We just missed to actually draft up the issue; thus, thanks for making it so. :-) Once we start working on this feature, we will update this issue accordingly. So stay tuned!

smcvb avatar Apr 22 '20 13:04 smcvb

As shown above, I've mentioned this issue in #2417. The mentioned issue is in essence a duplicate of this issue. However, it contains quite some information and thought on the subject too. As such, I recommend we read both issues once development on aggregate state transitions starts.

smcvb avatar Oct 03 '22 16:10 smcvb

I gave this issue some thought and came up with some ideas (all annotation names and class/interface names are made up and can be renamed to something appropriate in the future - please comment on the main idea not the names!):

  • We have a generic "state validator" interface which takes the aggregate type as the type parameter.
    • Let's call it FiniteStateValidator<T>.
    • It defines a single method boolean validate(T aggregate)
  • We create an implementation for each possible state in the state machine.
    • e.g. for aggregate of type Order we would have:
      • class OrderCreated implements FiniteStateValidator<Order> { /* ... */}
      • class OrderPaid implements FiniteStateValidator<Order> { /* ... */}
      • etc.
    • The validate method would take the latest aggregate as an argument then perform logic on it to determine whether it was indeed in the given state. Obviously you would need to either make your implementation class a static inner class if the fields/methods you want access to are private, or you can put it in the same package if they are package private.
  • We can then place some annotation on top of command handlers within the aggregate.
    • Let's call it @StateMachineCheck(<some-class>) where <some-class> must be a class implementing FiniteStateValidator<Order>.
    • The framework having seen this annotation would do the leg work to wire up an interceptor that is invoked before the command is invoked to verify that the aggregate is indeed in that state of the finite state machine.
      • This would involve calling the validate method on the specific class for that state with the loaded aggregate passed as an argument.
    • Example annotation values on a @CommandHandler :
      • @StateMachineCheck(OrderCreated.class)
        • A @CommandHandler annotated with this will only run if the validate method of the OrderCreated class returned true.
        • If it does not return true the framework will throw an exception.
      • @StateMachineCheck(values = {OrderCreated.class, OrderPaid.class })
        • A @CommandHandler annotated with this will run if the aggregate is in the OrderCreated OR OrderPaid state.
          • So only one of the boolean methods needs to be true.
        • If neither method call to validate() returns true the framework will throw an exception.

At this point you may be asking yourself, what is the point? Just have the validate methods for each finite state in the aggregate class itself and then make sure you call the relevant methods in the body of your @CommandHandler. Yes that would work but the above annotations would signal intent and be easier to read. Plus it is only part of the picture.

In addition, now suppose we introduce a class level annotation which goes on the aggregate: @FiniteStateMachine. With this on the aggregate itself, the behaviour changes so that every single @CommandHandler must be annotated with a @StateMachineCheck (maybe all except the creational command handler).

By opting in to a @FiniteStateMachine you implicitly inherit an END state. If the aggregate is in the END state it automatically rejects all commands (there probably would need to be a special @CommandHandler that could be used to resurrect it as an extreme exception). This would solve the boiler plate issue mentioned here.

Now with every @CommandHandler being defined such that they know the state of the state machine they are in before being run we are still left to deal with how to define the state transitions:

  • That is the job of another annotation: @StateMachineTransitionsTo(<some-class>) where <some-class> must be a class implementing FiniteStateValidator<Order>.
  • This annotation goes on the @EventSourcingHandler.
  • The validate method could be called after running the @EventSourcingHandler to validate it has indeed transitioned to that state in the state machine.
  • You could even make it part of the transaction so that the event is only persisted if the state transition is successful.

Now with introspection you have the full finite state machine and the allowed operations (commands) for each finite state, as well as every event which transitions the states from one to another.


I just thought of this and wanted to share in case it is useful. I know this type of feature would be low priority anyway 👍

vab2048 avatar May 05 '23 02:05 vab2048