When NoHandlerForCommandException is detected, what is the proper way to compensate the previous operation in Saga?

Hi Everyone,

If I have a Saga with 2 services attached to Axon Server, but one of the services is down. When the Saga starts and sends the next CreateInvoiceCommand to commandGateway, it will throw NoHandlerForCommandException. I am wondering what is the proper way to compensate for previously executed command?

How can I let the caller OrderCommandServiceImpl.createOrderAndWait() method knows that the saga transaction is failed and is compensated?

@Service
public class OrderCommandServiceImpl implements OrderCommandService {

    private final CommandGateway commandGateway;
    
    @Autowired
    private MyOrderService myOrderService;

    public OrderCommandServiceImpl(CommandGateway commandGateway) {
        this.commandGateway = commandGateway;
    }
    
    @Override
    public String createOrderAndWait(OrderCreateDTO orderCreateDTO) {
    	myOrderService.incrementOrder();
        return commandGateway.sendAndWait(new CreateOrderCommand(UUID.randomUUID().toString(), orderCreateDTO.getItemType(),
                orderCreateDTO.getPrice(), orderCreateDTO.getCurrency(), String.valueOf(OrderStatus.CREATED)));
    }
}
@Aggregate
public class OrderAggregate {

	private MyOrderService myOrderService;
	
    @AggregateIdentifier
    private String orderId;

    private ItemType itemType;

    private BigDecimal price;

    private String currency;

    private OrderStatus orderStatus;

    public OrderAggregate() {
    }

    @CommandHandler
    public OrderAggregate(CreateOrderCommand createOrderCommand, MyOrderService myOrderService){
    	this.myOrderService = myOrderService;
        AggregateLifecycle.apply(new OrderCreatedEvent(createOrderCommand.orderId, createOrderCommand.itemType,
                createOrderCommand.price, createOrderCommand.currency, createOrderCommand.orderStatus));
    }

    @EventSourcingHandler
    protected void on(OrderCreatedEvent orderCreatedEvent){
        this.orderId = orderCreatedEvent.orderId;
        this.itemType = ItemType.valueOf(orderCreatedEvent.itemType);
        this.price = orderCreatedEvent.price;
        this.currency = orderCreatedEvent.currency;
        this.orderStatus = OrderStatus.valueOf(orderCreatedEvent.orderStatus);
    }

@Saga
public class OrderManagementSaga {

    @Inject
    private transient CommandGateway commandGateway;

    @StartSaga
    @SagaEventHandler(associationProperty = "orderId")
    public void handle(OrderCreatedEvent orderCreatedEvent){
        String paymentId = UUID.randomUUID().toString();
        System.out.println("Saga OrderCreatedEvent Received");

        //associate Saga
        SagaLifecycle.associateWith("paymentId", paymentId);

        System.out.println("order id" + orderCreatedEvent.orderId);

        //send the commands
        try {
        	commandGateway.sendAndWait(new CreateInvoiceCommand(paymentId, orderCreatedEvent.orderId));
        } catch (AxonException ae) {
        	System.out.println("Caught Exception " + ae);
        	commandGateway.sendAndWait(new UpdateOrderStatusCommand(orderCreatedEvent.orderId, String.valueOf(OrderStatus.REJECTED)));
        }
        
    }

...

Hi @benpau! Welcome to the forum.

When it comes to dealing with failures in your sagas, you can compensate through roughtly the following approaches:

  • Dispatch another command to compensate the failed task.
  • Schedule an event to compensate the failed task at a later moment in time.
  • Schedule a deadline to compensate the failed task at a later moment in time.

Just to be clear, I wouldn’t call the failed CreateInvoiceCommand in this scenario something you would want to compensate. You would rather want to retry this at a later stage, given the fact there is no handler for said command at that moment in time.

Hence, scheduling a trigger for a later moment in time, may be better.

Granted, I would consider getting a NoHandlerForCommandException rather problematic.
So, increasing the number of instances handling said command for fault tolerance would be a good move to.

I hope that helps you further, @benpau!