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?
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.
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:
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.
Sometimes, ensuring atomicity is not possible.
If you need to store your aggregate in several tables, there are some solutions:
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.
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