2
votes

This is more of a theoretical question related to CQRS and Axon. Below is a self-explanatory setup, the code is a pseudo-code, it is not meant to compile.

Assuming that in order to process a command from an aggregate "Foo" we need first to query a state of another aggregate "Bar" for validation (from another bounded context, so it's not a question of a simple lookup of the "Foo"'s member aggregates).

We have two choices here as illustrated in the pseudo-code. Choice (1), we just run the query from the command handler in the "Foo" aggregate using the query gateway.

Choice (2), we apply an event asking a dedicated service to handle the querying instead of "Foo". The service then will dispatch a command back to "Foo" after it has queried the system for the state of "Bar".

Does the first choice (querying directly from the command handler) seems to go against the whole idea of Command-Query separation -- after all, this way we are executing a query during a command processing, right?

The second choice seems more in the spirit of CQRS: the command just results in an event (which later will result in an another command, etc.). But obviously, this comes at a cost: there are more steps involved, 2a, 2b, 2c, 2d...

I would like to hear what the community thinks about this. For me, it seems bizarre if we are strict about not mixing commands with query processing but are allowing executing queries with command processing. Or am I missing something?

@Aggregate
class AggregateFoo {

    private QueryGateway queryGateway;

    @CommandHandler
    public void on(UpdateFooCommand command){

        /*
            Assume that in order to validate this command we first need
            to query the state of another aggregate, "Bar".
        */

        // 1. We can just issue the query directly from the command handler.

        queryGateway
            .query(new AskForStateOfBarQuery(command.getBarId()))
            .then(queryResponse -> {
                // Proceed with original command execution depending
                // the result of the query response.    
            });

        // 2a. Or we can issue an intermediate EVENT offloading the query handling
        //     to a dedicated service ("FooBarService", see below).

        AggregateLifecycle.apply(new FooUpdateValidationRequestedEvent(command.getBarId()));

    }

    // 2d. "Foo" aggregate will react to the validation command from the
    //     dedicated service effectively executing the original command.

    @CommandHandler
    public void on(ProceedWithFooUpdateCommand command){

        // Do other validations, issue events here. At this
        // point we know that UpdateFooCommand was validated.
    } 

}

@Service
class FooBarService {

    private QueryGateway queryGateway;
    private CommandGateway commandGateway;

    @EventHandler
    public void on(FooUpdateValidationRequestedEvent event){
        
        // 2b. The dedicated service will run the corresponding query,
        //     asking for the state of "Bar".
        
        queryGateway
            .query(new AskForStateOfBarQuery(command.getBarId()))
            .then(queryResponse -> {

                // 2c. And will issue a COMMAND to the "Foo" aggregate
                //     indicating that it shoud proceed with the original 
                //     command's (UpdateFooCommand) execution.

                commandGateway.send(new ProceedWithFooUpdateCommand(command.getFooId()));

            });

    }

}

UPDATE:

This is an update after the discussion of the informative answer given by 91stefan (see below).

class AggregateFoo {
    int f = 9;

    // reference to the related Bar aggregate
    UUID bar;

    on(UpdateFooCommand){
        // Assume we must execute ONLY IF f < 10 AND bar.b > 10.
        // So we apply event to Saga (with f = 9), 
        // Saga will ask Bar: (b = 15), so the condition holds
        // and Saga issues ConfirmValidBarStateCommand
    }

    // Meanwhile, when Saga is busy validating, we process another
    // command CHANGING the state of Foo
    on(AnotherCommand) { f++; }

    // or "ConfirmValidBarStateCommand" as in 91stefan's example
    on(ProceedWithFooUpdateCommand){
        // So when we get back here (from Saga), there is no guarantee
        // that the actual state of Foo (f < 10) still holds,
        // and that we can proceed with the execution of the
        // original UpdateFooCommand
    }

}

class AggregateBar {
    int b = 15;
}

So looks like the question still holds: how to validate and execute a command consistently in Foo if its validation depends on the state of Bar from another bounded context? Looks like we may have several choices here:

  • direct query from Foo's command handler for a projected state of Bar
  • event to a (stateless) service which queries Bar for a projected state
  • (91stefan's suggestion) event to a Saga which sends validation command to Bar
1

1 Answers

2
votes

You might face issues, querying from the command handler is not considered a good practice as your projection might not be up-to-date due to eventual consistency. Also, command handlers from same aggregate are executed sequentially/synchronously. You just want to do quick stuff, calling external services will block and prevent other commands from aggregate from executing until the current command handler finishes execution.

What you need in this case is Saga. https://docs.axoniq.io/reference-guide/v/3.3/part-ii-domain-logic/sagas

Simplify flow would be:

  • commandHandler(UpdateFooCommand) -> applyEvent(AskedForStateOfBarEvent)
  • AskedForStateOfBarEvent starts Saga
  • Inside saga, send command to Bar aggregate -> ConfirmYourStateBar(barAggregateId)
  • commandHandler(ConfirmYourStateBar) -> validate state -> applyEvent(StateIsValidatedEvent)
  • Saga reacts to StateIsValidatedEvent, sends command to AggregateFoo -> (ConfirmValidBarStateCommand(aggreageFooId) & saga is closed (end event is StateIsValidatedEvent)
  • In commandHandler(ConfirmValidBarStateCommand) you proceed your execution, as your state BAR state is valid