0

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:

  1. 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.

  2. 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 than CouldNotAddException is.

final class RepositoryOperationResult 
{
    public function succeeded(): bool;

    public function getFailureCause(): ?Exception

    public function getFailureMessage(): ?string
}

1 Answer 1

1

I find it's usually a matter of style and language support. I prefer to throw True Exceptions and handle errors in messaging. Be wary of using Exceptions to signal status results. Exceptions should be Exceptional. Some languages like Go support intrinsic error passing, which is pretty nifty once you get the hang of it, but that's not always an option and trying to introduce it as a paradigm could cause some major architectural shifts for your stack.

It might also help to differentiate between the causes of the failure: Is a save action failing because of network issues? Because that's different than a save that fails because it violates some FK constraint.The former, if it can't be recovered from or backed-off on, that's probably an exception that should be thrown. But the later? That might be a part of your status messaging that could be used to update your response messaging and let the client know they did something wrong. An Exception might necessitate tossing the entire state/form/view, but messaging might only require a user to update a field.

In the end, remember YAGNI. A general Exception clause is fine early on in development when you might not have yet defined all the ways and whys a thing can fail. Just leave room for it to grow and don't forget your tests.

2
  • I agree with you particularly on saving Exceptions for Exceptional circumstances, which I did try to communicate in my question i.e., handling unpredicted failure which is IMO exceptional behavior. I appreciate the response though I do not feel it answers my question of how to communicate exceptional circumstances from a Domain-layer component (the repository contract) without making the client code (e.g., application service) privy to the technical implementation. Are you suggesting that my domain-layer CouldNotSaveException accomplishes this? I agree w/YAGNI and general exceptions during dev
    – John Hall
    Commented Jul 19, 2023 at 19:04
  • Right on. So, w/ Errors vs. Exceptions aside and I think it's a given you understand that the Exception is designed against the interface, not the implementation, so next is to consider how a consumer recovers from that Exception. ex: Did the exception arise from a network connectivity issue or an authorization issue? Those are things the consumer might be able to recover from (backoff strats or login redirects), so maybe we need a 'NetworkFailureException' or 'AuthorizationException' or just an 'ErrorId' prop. The other thing to consider is how those exceptions appear in logging. Commented Jul 21, 2023 at 14:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.