I'm reading Vaughn Vernon Book - Implementing Domain Driven Design. There is an example of a Project Management Application. There are aggregates like BacklogItem, Sprint, etc. If I have BacklogItemNotFoundException defined in Domain layer. Should my Rest adapter catch it and transform into NotFoundHttpResult? Or any other broken invariant exceptions like: EmailPatternBrokenException or TooManyCharactersForNameException or whatever should be handled in Rest adapter(ports&adapters architecture) and re-transformed into rest responses? If yes, it means that RestAdapter should have a reference to Domain layer? This is what bothers me...
The question is a contradiction. If it is a Domain Exception, it means that it is thrown by the domain.
Anyway, exceptions thrown by the domain should be handled by the application layer.
I have an exception handler decorator for the command bus, that catch any domain exception and translates it into an Application Exception.
This application exception is thrown to the adapters.
Adapters know about application exceptions, not domain exceptions.
UPDATE
My domain exception is an abstract base class from which the concrte domain exceptions inherit
public abstract class DomainException extends RuntimeException {
private static final long serialVersionUID = 1L;
private ErrorMessage mainErrorMessage;
private List<ErrorMessage> detailErrorMessages;
protected DomainException ( List<ErrorMessage> aDetailMessages, Object... aMainMessageArgs ) {
this.mainErrorMessage = new ErrorMessage(this.getClass().getSimpleName(), aMainMessageArgs );
this.detailErrorMessages = ( (aDetailMessages==null) ? new ArrayList<ErrorMessage>() : aDetailMessages );
}
public ErrorMessage mainErrorMessage() {
return this.mainErrorMessage;
}
public List<ErrorMessage> detailErrorMessages() {
return this.detailErrorMessages;
}
}
ErrorMessage has a key and a list of args. The messages are in a property file where the key is the name of the concrete domain exception class.
Application exception is just one type, which holds the concrete text message.
public class ApplicationException extends Exception {
private static final long serialVersionUID = 1L;
private String mainMessage;
private String[] detailMessages = new String[0];
public ApplicationException ( String aMainMessage, Throwable aCause, String... aDetailMessages ) {
super ("Main Message = "+aMainMessage+" - DetailMessages = "+Utils.toString(aDetailMessages), aCause );
this.mainMessage = aMainMessage;
this.detailMessages = ( (aDetailMessages==null) ? (new String[0]) : aDetailMessages );
}
public String mainMessage() {
return this.mainMessage;
}
public boolean hasDetailMessages() {
return (this.detailMessages.length > 0);
}
public String[] detailMessages() {
return this.detailMessages;
}
}
I have a decorator (wraps the execution of every command) for handling domain exceptions:
public class DomainExceptionHandlerDecorator extends Decorator {
private final DomainExceptionHandler domainExceptionHandler;
public DomainExceptionHandlerDecorator (DomainExceptionHandler domainExceptionHandler) {
this.domainExceptionHandler = domainExceptionHandler;
}
@Override
public <C extends Command> void decorateCommand(Mediator mediator, C command) throws ApplicationException {
try {
mediator.executeCommand(command);
} catch ( DomainException de ) {
this.domainExceptionHandler.handle (de);
}
}
}
And I have a domain exception handler that takes a domain exception, translates it into an app exception by reading properties file (TextMessageService does the job) and throw the app exception.
public class TranslatorDomainExceptionHandler implements DomainExceptionHandler {
private final TextMessageService configurationService;
public TranslatorDomainExceptionHandler ( TextMessageService aConfigurationService ) {
this.configurationService = aConfigurationService;
}
@Override
public void handle ( DomainException de ) throws ApplicationException {
ErrorMessage mainErrorMessage = de.mainErrorMessage();
List<ErrorMessage> detailErrorMessages = de.detailErrorMessages();
String mainMessage = this.configurationService.mensajeDeError ( mainErrorMessage );
String[] detailMessages = new String [ detailErrorMessages.size() ];
int i = 0;
for ( ErrorMessage aDetailErrorMessage : detailErrorMessages ) {
detailMessages[i] = this.configurationService.mensajeDeError ( aDetailErrorMessage );
i++;
}
throw new ApplicationException ( mainMessage, de, detailMessages);
}
}
The adapter (an UI for example) will catch the app exception and show its message to the user. But it doesn't know about domain exceptions.
I try to avoid domain exceptions as much as I can and prefer to make invalid states unreachable instead. The first reason is that exceptions are for exceptional, unexpected things, the second that I don't like my code to be cluttered with fine-grained try/catches for every little business-ish thing that could go wrong.
BacklogItemNotFoundException
To me this is typically your Repository or query service returning null or an empty list. No need for a domain exception.
EmailPatternBrokenException
TooManyCharactersForNameException
I let the validation feature of my web framework handle these. You could also check it in the Domain but it will rarely reach that point and you don't really need to handle that kind of error specifically.
As a result, the two typical scenarios are:
+-----------------------+--------------------+-------------------------------------------------+
| Domain | Application | Presentation |
+-----------------------+--------------------+-------------------------------------------------+
| Expected failure case | Return Result.Fail | Clean error message |
+-----------------------+--------------------+-------------------------------------------------+
| Exception | - | Caught in catch-all clause > 500 error or other |
+-----------------------+--------------------+-------------------------------------------------+
I will add my 2 cents about error handling, not specifically related to DDD.
The exception are part of the contract you expose to the consumer. If you're expected to for example add an item to a shopping cart, the exception you may explicitly throw include itemNotAvailable, shoppingCartNotExisting, etc...
Technical exception on the other hand are not part of the contract, they may occurs but shouldn't be explicitly handled as no one can do anything about it, they must imply the operation interruption (and the rollback of the current unit of work).
A rest interface is a contract for an operation on a resource. When using rest over http the terms of the contract are related to the http protocol.
Typical operation described above (adding ie. post an item on a cart resource) would be translated to, for example, 404 for shoppingCartNotExisting and 409 for itemNotAvailable (conflict ie. the update on the resource is no more possible because some state has changed meantime).
So yes all "domain" exception (expected exceptions as part of the contract) should be explicitly mapped by the rest adapter, all unchecked ones should result in a 500 error.
TLDR; It is OK if the Application or Presentation layer has a dependency to the Domain layer, the other way is not recommended.
Idealy, there should not exist any dependency from one layer to another but that is impossible or the software would not be usable. Instead you should try to minimize the number and the direction of the dependencies. The general rule or best practice to a clean architecture is to keep the Domain layer agnostic of the infrastructure or the Application layer. The Domain objects (Aggregates, Value objects etc) should not care about a specific persistence or Rest or HTTP or MVC, just like the domain experts don't care about these things.
In real world, the Domain layer may be influenced by technology (like frameworks). For example we put annotations to mark some Domain objects as behaving in some specific way when persisted instead of using external XML or JSON files just because it is at hand, it is easier to maintain them. We need, however, to limit these influences to a minimum.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With