How should I handle exception in domain model?

In the following domain model, I don’t really want that RuntimeException, I need a proper way to be able to throw a normal exception, e.g. NotEnoughMoneyException, and be able to handle it at other places. What is a proper approach?

And further, if that WithdrawCommand is from a Rest controller, how can provide a user-friendly interface to the user, display a message “Not enough money in your account for this withdraw”?

public class Account extends AbstractAnnotatedAggregateRoot {

@CommandHandler
public void handle(WithdrawCommand cmd) {
log.info(“withdraw: id=” + id + “, balance=” + balance + “, amount=” + cmd.getAmount());
if (balance > cmd.getAmount()) {
apply(new AccountBalanceDecreasedEvent(id, balance, cmd.getAmount()));
} else {
log.info(“not enough money!”);
throw new RuntimeException(“not enough money”);
}
}

}

Do you need to throw exception? What about creating new NotEnoughMoneyEvent and handle it in your application?

You can throw any type of exception you want. Note that runtime exceptions (or subclasses) will cause a rollback of a transaction. Checked exceptions will not.

Use exceptions to indicate nothing happened as a result of a command. If an attempt is important in the domain (like a filed login attempt), you can (also) apply an event.

Cheers,

Allard

Thanks for the idea. The problem I got here is that the RuntimeException causes a rollback, the REST client receives a HTTP 500 (internal server error). This is bad (but at least the client knows somethign was wrong), but the client has no idea whether it is the server crashed or “not enough money”. If I apply an NotEnoughMoneyEvent as you suggested, the event handler cannot do much other than log the event, the REST client will get a HTTP 200 response and it seems that the withdraw was sucessful.

If I throw a checked exception like NotEnoughMoneyException, this is in a CommandHandler, where can I capture the exception?

When you dispatch the command, pass a callback as parameter to the invocation. The callback is invoked with the result of the command handler invocation.
If you use a CommandGateway, use sendAndWait and catch exceptions thrown, or define your own interface and declare checked exceptions on the method signatures (see reference guide for details).

Cheers,

Allard

Thanks Allard! I took your exception advice as follows, I only received an org.axonframework.commandhandling.CommandExecutionException, and NotEnoughMoneyException as a root cause. I can’t actually catch NotEnoughMoneyException. What did I do wrong?

In my REST controller:

try{
cmdGateway.sendAndWait(new WithdrawCommand (id, req.getAmount()), 3000, TimeUnit.MILLISECONDS);

} catch (NotEnoughMoneyException e) {
e.printStackTrace();
String err = “{“error”:”"+e.getLocalizedMessage() + “”}";
return new ResponseEntity(err, HttpStatus.BAD_REQUEST);
}

In the command handler:

@CommandHandler
public void handle(WithdrawCommand cmd) throws NotEnoughMoneyException{
log.info(“withdraw: id=” + id + “, balance=” + balance + “, amount=” + cmd.getAmount());
if (balance > cmd.getAmount()) {
apply(new AccountBalanceDecreasedEvent(id, balance, cmd.getAmount()));
} else {
log.info(“not enough money!”);
throw new NotEnoughMoneyException(“Not enough money to fulfill the withdraw.”);
}
}

org.axonframework.commandhandling.CommandExecutionException: Command execution resulted in a checked exception that was not declared on the gateway
at org.axonframework.commandhandling.gateway.GatewayProxyFactory$WrapNonDeclaredCheckedExceptions.invoke(GatewayProxyFactory.java:524)
at org.axonframework.commandhandling.gateway.GatewayProxyFactory$GatewayInvocationHandler.invoke(GatewayProxyFactory.java:423)
at com.sun.proxy.$Proxy79.sendAndWait(Unknown Source)

at java.lang.Thread.run(Thread.java:745)
Caused by: com.example.NotEnoughMoneyException: Not enough money to fulfill the withdraw.

I found a workaround on the issue of the advice Allard gave. This is very dependent on the behaviour of AxonFramework.

try{
cmdGateway.sendAndWait(new WithdrawCommand (id, req.getAmount()), 3000, TimeUnit.MILLISECONDS);

} catch (CommandExecutionException ex) {

Throwable e = ex.getCause();

if (e instanceof NotEnoughMoneyException ){

String err = “{“error”:”"+e.getLocalizedMessage() + “”}";
return new ResponseEntity(err, HttpStatus.BAD_REQUEST);

}
else{
//TODO
}
}

public class NotEnoughMoneyException extends CommandExecutionException {

public NotEnoughMoneyException () {
super(“Not enough money to fulfill the withdraw.”, null);
}

public NotEnoughMoneyException (String message, Throwable cause) {
super(message, cause);
// TODO Auto-generated constructor stub
}

}

What confuses me is this in the reference doc:

Exceptions have the following effect:

  • Any declared checked exception will be thrown if the Command Handler (or an interceptor) threw an exceptions of that type. If a checked exception is thrown that has not been declared, it is wrapped in a CommandExecutionException, which is a RuntimeException.

A declared a checked exception on the Command Handler is impossible to be caught on cmdGateway.sendAndWait(…). You can only declare a unchecked exception (RuntimeException), and it will be wrapped by the framework. So from the reference doc, it is not very clear what I should do.

Check out the paragraph before the bullets:

3.1.2. Creating a Custom Command Gateway

The GatewayProxyFactory creates an instance of a Command Gateway based on an interface class. The behavior of each method is based on the parameter types, return type and declared exception. Using this gateway is not only convenient, it makes testing a lot easier by allowing you to mock your interface where needed.

The default gateway doesn’t have support for (custom) checked exceptions. To do that, use the GatewayProxyFactory to create a gateway out of your own interface.

Cheers,

Allard