How to work with exceptions in DDD

    image

    In the framework of the recently held DotNext 2018 conference , the BoF on Domain Driven Design was held. It raised the issue of working with exceptions, which caused a heated debate, but did not get a detailed discussion, since it was not the main topic.

    Also, studying a lot of resources, ranging from questions on stackoverflow and ending with paid courses on architecture, one can observe that the IT community has developed an ambiguous attitude towards exceptions and how to use them.

    Most often it is mentioned that using exceptions it is easy to build a flow of execution that has the semantics of the goto operator , which has a bad effect on the readability of the code.

    There are different opinions on whether to create your own exception types.or use the standard supplied in .NET.

    Someone does validation on exceptions, and someone else uses the Result monad . It is fair that Result allows us to understand by the method signature whether it is possible not only successful execution. But it is no less true that in imperative languages ​​(which include C #), the widespread use of Result leads to poorly readable code, filled with language constructs so that it is difficult to discern the original script.

    In this article I will talk about the practices adopted in our team (if briefly - we use all the approaches and none of them is a dogma).

    It will be about enterprise-application built on the basis of ASP.NET MVC + WebAPI. The application is built on the onion architectureCommunicates with a database and message broker. Structured logging to the ELK stack is used and monitoring is configured with Grafana.

    We will look at work with exceptions from three angles:

    1. General rules for dealing with exceptions
    2. Exceptions, Errors, and Onion Architecture
    3. Special cases for web applications

    General rules for dealing with exceptions


    1. Exceptions and errors are not the same thing. For exceptions use exceptions, for errors - Result.
    2. Exceptions only for exceptional situations, which by definition can not be many. This means there are fewer exceptions - the better.
    3. Exception handling should be as granular as possible. As Richter wrote in his monumental work.
    4. If the error should be delivered to the user in its original form - use Result.
    5. The exception should not leave the system boundaries in their original form. It is not user friendly and gives the attacker a way to further explore possible system weaknesses.
    6. If the thrown exception is processed by our application, we use not exception, but Result. An implementation on exceptions will be a hidden goto statement and the worse the processing code is from the exception code of an exception, the worse it will be. Result, on the other hand, explicitly declares the possibility of an error and allows only “linear” processing.

    Exceptions, Errors, and Onion Architecture


    In the following sections, we will look at the responsibilities and rules for throwing / handling exceptions / errors for the following layers:

    • Application hosts
    • Infrastructure
    • Application services
    • Domain core

    Application host


    What is responsible

    • Composition root that customizes the entire application.
    • The boundary of interaction with the outside world - users, other services, scheduled launch.

    Since these are quite complex responsibilities, it is worthwhile to limit them. The remaining responsibility is given to the inner layers.

    How to handle errors from Result

    Transmits to the outside world, converting it into the appropriate format (for example, in the http response).

    As Result generates

    any way. This layer does not contain logic, so there is no place to generate errors.

    How to handle exceptions

    1. Hides the details and converts to a format suitable for sending to the outside world
    2. Logs.

    As throws exceptions

    No way, this layer is the outermost and contains no logic - there is no one to give it an exception.

    Infrastructure


    What is responsible

    1. Adapters to ports , or simply implementations of Domain-interfaces, giving access to the infrastructure - third-party services, databases, active directory, etc. This layer should be as “stupid” as possible and contain as little logic as possible.
    2. If necessary, it can act as Anti-corruption layer .

    How it handles errors from Result

    I don’t know providers for databases and other services working on the Result monad. However, some services operate on return codes. In this case, convert them to the Result format required by the port.

    How Result Generates

    In general, this layer does not contain logic, which means it does not generate errors. But in the case of use as an anti corruption layer, a variety of options are possible. For example, parsing exceptions from the legacy service and converting to Result those exceptions that are simple validation messages.

    How to handle exceptions

    In general, throws further, if necessary, pledge the details. If the port being implemented allows Result to be returned in the contract, then the infrastructure converts the types of exceptions that can be handled to Result.

    For example, a message broker used in a project throws exceptions when trying to send a message when the broker is unavailable. The Application Services layer is ready for this situation and is able to handle it with the policy of Retry, Circuit Breaker or manual data rollback.

    In this case, the Application Services layer declares a contract that returns a Result in case of an error. And the Infrastructure layer implements this port, converting the exception from the broker to Result. Naturally, converts only specific types of exceptions, and not everything.

    Using this approach, we get two advantages:

    1. Explicitly declare the possibility of errors in the contract.
    2. We get rid of the situation when the Application Service knows how to handle the error, but does not know the type of the exception, because it is abstracted from a particular message broker. At the same time, building a catch block on the base System.Exception means to capture all types of exceptions, and not just those that the Application Service can handle.

    How throws an exception

    Depends on the specifics of the system.

    For example, the LINQ operators Single and First, when requesting non-existent data, throw an InvalidOperationException exception. But this type of exception is used everywhere in .NET, which makes it impossible to perform its processing in granular form.

    We in the team took the practice of creating custom ItemNotFoundException and throwing it from the infrastructure layer if the requested data is not found and should not be according to business rules.

    If the requested data is not found and it is permissible, it should be explicitly declared in the port contract. For example, using the Maybe monad .

    Application services


    What is responsible

    1. Validation of input data.
    2. Orchestration and coordination of services - starting and completing transactions, implementing distributed scripts, etc.
    3. Loading domain-objects and external data through the ports to the Infrastructure, the subsequent call commands in the Domain Core.

    How to handle errors from Result

    Errors from the domain core translates to the outside world without changes. Errors from Infrastructure can handle through policies Retry, Circuit Breaker or broadcast outside.

    How to generate a Result

    It can implement validation in the form of a Result.

    Can generate notifications of partial success of the operation. For example, messages to the user of the form “Your order was successfully placed, but an error occurred while verifying the delivery address. A specialist will contact you soon in order to clarify the details of the delivery. ”

    How does the exception handle

    Assuming that the infrastructure exceptions that the application is able to handle have already been converted by the Infrastructure layer to Result — it doesn’t handle it at all.

    How throws exceptions

    In general, no way. But there are borderline options described in the final section of the article.

    Domain core


    What is responsible for the

    implementation of business logic, the "core" of the system and the main purpose of its existence.

    How to handle errors from Result

    Since the layer is internal and errors are possible only from objects in the same domain, processing is reduced either to business rules or to broadcasting the error to the top in its original form.

    How Result Generates

    When Business Rules are Violated, which are encapsulated in Domain Core and not covered by validation of input data at the Application Services level. Generally in this layer Result is used most often.

    How to handle exceptions

    No Exceptions to the infrastructure have already been processed by the Infrastructure layer, the data have already come structured, complete and proven thanks to the Application Services layer. Accordingly, all the exceptions that can take off will be really exceptions.

    How to throw exceptions

    Usually a general rule works here: the fewer exceptions, the better.

    But have you ever had a situation where you write code and understand that under certain conditions it can mess things up? For example, twice to write off the money or so spoil the data that then we will not collect the bones.

    As a rule, we are talking about the execution of commands that are unacceptable for the current state of the object.

    Of course, the corresponding button on the UI should not be visible in this state. We should not get a command from the bus in this state. All this is true provided that the outer layers and systems performed their function normally . But in Domain Core we should not be aware of the existence of external layers and believe in the correctness of their work, we must protect the invariants of the system.

    Some of the checks can be placed in Application Services at the level of validation. But this can turn into defensive programming , which in extreme cases leads to the following:

    1. Encapsulation is weakened, since certain invariants must be tested on the outer layer.
    2. Knowledge of the subject area “flows” into the outer layer, checks can be duplicated by both layers.
    3. Testing the validity of executing a command from the outer layer can be more complicated and less reliable than checking the domain object with the impossibility of executing a command in the current state.

    Also, if we place such checks in the validation layer, then we must tell the user the reason for the error. Considering that we are talking about an operation that cannot be performed under current conditions at all, we risk being in one of two situations of a situation:

    • We gave the ordinary user a message that he didn’t understand at all and would still go to support, as with the message "An unexpected error occurred."
    • We quite clearly told the villain why he could not perform the operation he wanted to perform and he could look for other workarounds.

    But back to the main topic of the article. By all indications, the situation under discussion is exceptional. It should never happen, but if it does, it will be bad.

    In this situation, it is most logical to throw an exception, log the necessary details, return the general error “Operation Impossible” to the user, set up monitoring for this type of errors and expect that we will never see them.

    What type or types of exceptions to use in this case? Logically, it should be a separate type of exception, so that we can distinguish it from others, and so that it does not accidentally be hooked by exception handling from the outer layer. We, too, do not need a hierarchy or many exceptions, the essence is the same — something unacceptable has happened. We in our projects create for this the type CorruptedInvariantException, and use it in appropriate situations.

    Special cases for web applications


    A significant difference of web applications from others (desktop, demons and windows services, etc.) is interaction with the outside world in the form of short-term operations (processing HTTP requests), after which the application “forgets” about what happened.

    Also, after the completion of the processing of a request, a response is always generated. If the operation performed by our code does not return data, the platform will still return a response containing the status code. If the operation was interrupted by an exception, the platform will still return a response containing the corresponding status code.

    To implement this behavior, processing requests in Web platforms is built in the form of pipelines (pipe). First, the request is processed sequentially and then the response is prepared.

    We can use middleware, action filter, http handler or ISAPI filter (depending on the platform) and integrate into this pipeline at any stage. And at any stage of processing the request, we can interrupt processing and the pipeline will proceed to the formation of a response.

    We usually implement the business part of the application not in the pipeline architecture, but write the code that performs the operations sequentially. And with this approach, it is somewhat more difficult to implement the script when we interrupt the execution of the request and immediately proceed to form the answer.

    What does all this have to do with exception handling, you ask?

    The fact is that the rules for working with exceptions described in the previous parts of the article do not fit well in this scenario.

    It is bad to use exceptions because it is goto semantics.

    Ubiquitous use of Result leads to the fact that we drag it (Result) across all layers of the application, and when forming the answer, we need to sort Result somehow in order to understand which return status code. It is also desirable to summarize and parse this parsing code in Middleware or ActionFilter, which becomes a separate adventure. That is, Result is not much better than exceptions.

    What to do in this situation?

    Do not build an absolute. We set the rules for our own benefit, not harm.

    If it is necessary to interrupt the operation, because its continuation is impossible, then an exception throw will not have goto semantics. We direct execution to the output, and not to another block of business code.

    If the reason for the interruption is important to determine the desired status code, you can use custom types of exceptions.

    Earlier, we mentioned two custom types that we use: ItemNotFoundException (transform into 404) and CorruptedInvariant (transform into 500).

    If you check the rights of users, because they do not fall on the role model or claim, then it is permissible to create a custom ForbiddenException (Status code 403).

    And finally, validation. We still can not do anything until the user modifies his query, this semantics is described by code 422 . So we interrupt the operation and send the request straight to the output. It is also valid to do using exception. For example, the FluentValidation library already has a built-in exception type., which sends to the client all the details necessary to clearly display to the user what is wrong with the request.

    That's all. And how do you work with exceptions?

    Also popular now: