Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What approach is there for handling and returning errors (non-exceptional and exceptional) in Domain Driven Design entities and aggregate roots?

I'm trying to find a good article/examples of how DDD entities treat errors (and what would be considered exceptional errors and what wouldn't) and how they pass them up to the calling application layer (which usually wraps operations in a transaction that would need to be rolled back).

Currently I'm thinking to consider all errors that would break the transaction of an aggregate (such as validation) to be exceptions. This way I can rollback the transaction in a "catch" block. For example:

SomeApplicationService:

// start transaction here
// ...

try 
{
    $user = $userRepository->userOfId($id);
    $user->doSomething();
    $user->doSomethingElse();  // <-- imagine an error thrown here
    $userRepository->save($user);
} 
catch (CustomFriendlyForUIException $e)
{
    // Custom Friendly for UI error
    // Rollback transaction and add error/message to UI payload DTO
    // ...
}
catch (AnotherCustomException $e)
{
    // Not friendly to UI, so use general error message for UI
    // Rollback transaction and add error/message to UI payload DTO
    // ...
}
catch (Exception $e)
{
    // Catch all other exceptions, use general error message for UI
    // Rollback transaction and add error/message to UI payload DTO
    // ...
}

// end transaction

Is this the correct approach, or am I missing something?

like image 814
prograhammer Avatar asked Aug 25 '15 00:08

prograhammer


1 Answers

Usually, there are two types of error:

  • business errors that have a business meaning. For instance, StockFullError, ProductNotAvailableError, etc. These errors are expected because they are explicitly created in the code.

    These kind of error can be implemented using exceptions or using the functional way: explicitly show errors in the return type. For instance: Either<Error, T>.

    Exception make the code simpler. Handling error the functional programming way make the code easier to reason about and more predictable. I would advise the latter.

  • infrastructure errors are errors linked to the database, network, external services, etc. These error are usually unexpected.

Error happen. And when error happen, you don't want the application to be in an inconsistent state. For instance, you want to avoid to have half of the aggregate stored in the datastore.

Aggregate integrity

The key problem here is to maintain the aggregate integrity. The critical point is when the aggregate has to be written in the datastore. I can see two situations:

The operation to persist the aggregate is atomic

As soon as your datastore can provide atomic operations, there is no worry to have about the data integrity because the result of an atomic operation is either a failure or a success. There is no partial writes.

That means that you can just let your http layer handle exception and return 500 for instance.

The operation to persist the aggregate is not atomic

Sometimes, ensuring atomicity is not possible.

One aggregate in several tables

If you need to store your aggregate in several tables, there are some solutions:

  • Transactions. Surrounding the code in a transaction could be a solution. However transactions have some disadvantages. If the computation is too long or if this part of the code is called too often, it would slow down the application.
  • Unit Of Work is an old pattern. The idea is to register operations that you want to do: add, update, delete, etc. Eventually, UOW applies the changes to the database using a transaction. See Fowler's article for more information.

SAGAs: sometimes a big transaction is not possible

When you send an email and save something in the database, you can't include the email in the transaction.

In that case, you can use SAGAs. The idea is that, sometime, you can't have one single big transaction to enforce atomicity. However, it is usually easy to have several small transactions.

The idea of a SAGA is to associate every single transaction to a compensating transaction. For instance given the "transaction": send an email to confirm that the product is bought, the compensating "transaction" could be sent an email to apologise with a coupon.

Every transaction small transaction is run. If one of these fails, compensating transaction are run. Eventually, this enables to get atomicity.

like image 139
Dnomyar Avatar answered Sep 22 '22 10:09

Dnomyar