My application follows Clean Architecture wherein the Application Layer wraps the Domain Layer. I try to adhere to DDD more-so as a "guiding light" than a strict rulebook.
Within the Domain Layer, Repository contracts are defined as interfaces. Those are implemented in the Infrastructure layer with various technologies.
My question is, as-per title, how to communicate unexpected/unpredictable failures occuring in the Repository implementation so that the Application Layer can react (e.g., log, manage transactions, notify user, etc.), without needing to be aware of the implementation detail i.e., the Repository technology. For unexpected/unpredicted failures I mean things like locks, limits, timeouts, or other cases that are not caused by e.g., malformed data or other circumstances that can/should be addresses before entering the Domain.
My current approach is definining CouldNot***Exception
(e.g., CouldNotAdd
, CouldNotRemove
) within the Domain layer and wrapping the implementation-specific Exception in that... but these persistence-related words are not part of the Ubiquitous Language and therefore this approach feels wrong... like I've created the Domain-layer exception class strictly so that the Repository Interface has some other Domain-layer component to communicate failure to its client.
In case this isn't clear, here is an example of how I have it now:
namepace Acme\Domain;
interface OrderRepositoryInterface {
/** @throws Domain\CouldNotAddException */
public function add(Order $order);
}
namesapce Acme\Application;
class OrderService {
private OrderRepositoryInterface $repository;
public function placeOrder($details)
{
// ...
try {
$this->transactionManager->begin();
$this->repository->add($order);
$this->eventBus->dispatch('order_placed', $order);
$this->transactionManager->commit();
} catch (Domain\CouldNotAddException $e) {
$this->transactionManager->rollback();
$this->logger->error($e);
}
}
}
// Example implementation #1
namespace Acme\Infrastructure;
class PDOOrderRepository implements OrderRepositoryInterface {
public function add(Order $order)
{
try {
// ...
} catch (PDOException $e) {
throw new CouldNotSaveException($e);
}
}
}
// Example implementation #2
namespace Acme\Infrastructure;
class MongoDbOrderRepository implements OrderRepositoryInterface
{
public function add(Order $order)
{
try {
// ...
} catch (MongoDbException $e) {
throw new CouldNotSaveException($e);
}
}
}
I've considered a couple alternatives. Those are:
Remove domain-layer
CouldNotAddException
(and similar) and change application layer code to a more broad catch clause such as\Exception
. This feels bad, but maybe only because I always like catching more specific Exceptions. Maybe this is a good reason to break this "general best practice" for the "lesser of two evils" i.e., ridding the Domain of the non-ubiquitous langauge. The major downside for this approach is that I'm no longer certain that I'm catching an error originating from the Repository implementation, though maybe this isn't a problem at all.Introduce a class to encapsulate the result of certain Repository actions (namely those that manipulate the Repository contents). This just feels like redefining the problem... after all,
RepositoryOperationResult
is no more part of the Ubiquitous Langauge thanCouldNotAddException
is.
final class RepositoryOperationResult
{
public function succeeded(): bool;
public function getFailureCause(): ?Exception
public function getFailureMessage(): ?string
}