data provided by an user is somehow incorrect and could cause my domain object to get into invalid state. So a subclass of DomainException extends RuntimeException is thrown.
Perfetto.
Now: how should I handle this? Suppose my endpoint is used by a frontend and I want to display a proper message to user.
Rileva l'eccezione di runtime sul limite di errore e utilizza il tipo e il contenuto dell'eccezione per creare una risposta appropriata da restituire al client.
Il limite di errore probabilmente appartiene al luogo in cui viene gestita la richiesta e si ha consapevolezza di cose come il tipo di media previsto.
Non sarebbe del tutto irragionevole avere un limite di errore nel gestore dei comandi, per pacchettizzare DomainException come ApplicationException, a seconda di come si percepisce il dominio che perde nel livello http. Poiché la convalida del comando avviene nel componente dell'applicazione, probabilmente vorrete essere in grado di generare eccezioni anche da lì. Entrambe queste ApplicationException verranno quindi gestite dal limite di errore nel componente che gestisce la richiesta http.
And since it's not a checked exception, how should I handle it? Try/Catch for a runtime exception seems to be awful.
Abbraccia la verità che controlla che le eccezioni siano un esperimento che non è stato davvero visualizzato; prendere e segnalare il problema.
Le DomainExceptions devono essere segnalate con codici di stato 4xx (es .: errori del client). 409-Conflict è quello che preferirei quando il problema è una violazione dell'invarianza di business.
Si sta dicendo al cliente che è probabile che possa risolvere il problema (ad esempio: ottenere una nuova visualizzazione dello stato di aggregazione e quindi modificare il comando in modo che non sia in conflitto). Quindi sarebbe ragionevole includere nella risposta ipermedia un link ad una rappresentazione leggibile dello stato aggregato, per esempio. Il che implicherebbe che la gestione delle eccezioni debba avvenire in un punto del codice che sappia come costruire quel collegamento dalle informazioni nella richiesta.
In pseudo codice, probabilmente l'intera cosa sembra
try {
// If this bit fails, we want to return a 400-Bad Request to the client.
Command<A> command = parse(httpRequest);
// We got this far, so now we pass the command we've parsed off to the
// command handler....
{
// The aggregate lookup could fail; that might be any of
// 400-Bad Request
// 404-Not Found
// 410-Gone
// ... depending on how you interpret the relationship between the
// *resource* and the *aggregate*
Aggregate<A> aggregate = Repository<A>.getById(command.id);
// A failure here is probably best reported via 409-Conflict;
// essentially to inform the user that the application state
// is stale, and should be refreshed
aggregate.execute(command);
// A failure here indicates that the command lost a data race;
// that's certainly a conflict. So if you are going to report
// an error at this point, 409-Conflict is the right way to go.
// A better choice might be to retry the command, though; after
// all, the stale version of the aggregate thought it was acceptable
// so the updated version might as well. In that case, you would
// catch this exception and reload the aggregate.
Repository<A>.save(aggregate);
}
// OK, we got through everything without a problem, so report success
reportSuccess(httpResponse);
} catch (...) {
// Possibly a single handler, or multiple handlers, depending upon
// how you design your exception hierarchy. The important thing is that
// the exception handler here knows how to translate the application
// exceptions thrown above into something suitable for http, and the
// media types being used by this endpoint.
exceptionHandler.forRequest(httpRequest).report(e, httpResponse);
}