25

I'm learning DDD and I'm thinking about throwing exceptions in certain situations. I understand that an object can not enter to a bad state so here the exceptions are fine, but in many examples exceptions are throwing also for example if we are trying add new user with exists email in database.

public function doIt(UserData $userData)
{
    $user = $this->userRepository->byEmail($userData->email());
    if ($user) {
        throw new UserAlreadyExistsException();
    }

    $this->userRepository->add(
        new User(
            $userData->email(),
            $userData->password()
        )
    );
}

So if user with this email exists, then we can in application service catch a exception, but we shouldn't control the operation of the application using the try-catch block.

How is best way for this?

1
  • 1
    Having domain service (handlers) throwing exceptions is fine.
    – Andy
    Commented Mar 2, 2018 at 18:37

2 Answers 2

36

Let's begin this with a short review of the problem space: One of the fundamental principals of DDD is to place the business rules as closely as possible to the places where they need to be enforced. This is an extremely important concept because it makes your system more "cohesive". Moving rules "upwards" is generally a sign of an anemic model; where the objects are just bags of data and the rules are injected with that data to be enforced.

An anemic model can make a lot of sense to developers just starting out with DDD. You create a User model and an EmailMustBeUniqueRule object that gets injected with the necessary information to validate the email. Simple. Elegant. The issue is that this "kind" of thinking is fundamentally procedural in nature. Not DDD. What ends up happening is you are left with a module with dozens of Rules neatly wrapped and encapsulated, but they are completely devoid of context to the point where they can no longer be changed because it's not clear when/where they are enforced. Does that make sense? It may be self-evident that a EmailMustBeUniqueRule will be applied on the creation of a User, but what about UserIsInGoodStandingRule ? Slowly but surely, the granularization of extracting the Rules out of their context leaves you with a system that is hard to understand (and thus cannot be changed). Rules should be encapsulated only when the actual crunching/execution is so verbose that your model starts to loose focus.

Now on to your specific question: The issue with having the Service/CommandHandler throw the Exception is that your business logic is starting to leak ("upwards") out of your domain. Why does your Service/CommandHandler need to know an email must be unique? The application service layer is generally used for coordination rather than implementation. The reason for this can be illustrated simply if we add a ChangeEmail method/command to your system. Now BOTH methods/command handlers would need to include your unique check. This is where a developer may be tempted to "extract" an EmailMustBeUniqueRule. As explained above, we don't want to go that route.

Some additional knowledge crunching can lead us to a more DDD answer. The uniqueness of an email is an invariant that must be enforced across a collection of User objects. Is there a concept in your domain that represents a "collection of User objects"? I think you can probably see where I'm going here.

For this particular case (and many more involving enforcing invariants across collections) the best place to implement this logic will be in your Repository. This is especially convenient because your Repository also "knows" the extra piece of infrastructure necessary to execute this kind of validation (the data store). In your case, I would place this check in the add method. This makes sense right? Conceptually, it is this method that truly adds a User to your system. The data store is an implementation detail.

EDIT:

Maybe persist or save are more suitable terms than add.

I also want to make clear that the specific method trace within which the "user is unique" rule is enforced is not really important. My point above is that the invariant is best-suited to be enforced at the same conceptual place in which it exists. That is, your "collection of all users". How the validation occurs is less important. The interface for our repository's add (or persist or save) is simply "indicate success or throw exception". Whether the the actual crunching of our rule is handled in the repository's definition (e.g. exists) or handled by the data source (e.g. a unique database index) is immaterial given our signature.

Now I know what you are thinking, "That's moving a business rule out of the domain. Now I can't switch data sources!". Well consider this: whatever test you are currently using to verify that no two User entities can exists with the same email should catch this for you. And if you don't have a such a test, then you don't really have a rule, do you?

Leaning on my comment from below: Set validation is tricky, and often doesn't play well with DDD. You are left with either choosing to validate that some process will work before attempting that process, or accepting that this specific kind of invariant won't be neatly encapsulated in your domain. The former represents a separation of data and behavior resulting in a procedural paradigm (and race conditions if not handled appropriately). This is less than ideal. Validation should exist with data, not around data.

9
  • 3
    “Upwards” means towards your application layer (think of a layered architecture). I touched on this above, but the reason moving rules upwards is a sign of an anemic model is that it represents the separation of data and behavior in a way that defeats the entire purpose of having a core domain model. If validating an email is in a separate module than sending an email, your Email model must be a bag of data that is inspected for invariants before sending. See: softwareengineering.stackexchange.com/questions/372338/… Commented Sep 3, 2018 at 14:26
  • 6
    Moving Domain Logic To Repository is wrong . you should enforce domain rule by aggregate root in domain layer .
    – Arash
    Commented Nov 19, 2018 at 11:29
  • 2
    @Arash Set validation is tricky, and often doesn't play well with DDD. You are left with either choosing to validate that some process will work before attempting that process (adding a User), or accepting that this specific kind of invariant won't be neatly encapsulated in your domain. The former represents a separation of data and behavior resulting in a procedural paradigm. This is less than ideal. Validation should exist with data, not around data. Furthermore, what benefit is there to create a domain service that only serves to check this one rule? Ostensibly, this service ... Commented Nov 19, 2018 at 17:59
  • 3
    @Arash couples your Repository, User, and this invariant and therefore does not achieve the purist vision you are pushing for anyway. Repository implementations are part of the domain, and therefore can be given certain responsibilities. Commented Nov 19, 2018 at 18:04
  • 3
    @king-side-slide what do you mean repository implementations are part of the domain? It’s the interfaces that are part of the domain. Implementation is part of the infrastructure layer, otherwise the domain wouldn’t be isolated from persistence concerns. Commented Oct 9, 2020 at 12:26
8

I agree email uniqueness logic like this belongs in the domain: it's a universal business rule that should be enforced universally regardless of the application layer(s) built on top of it.

However, I disagree that the repo is the right place. Here’s my reasoning:

  • The repo interfaces are part of the domain - it needs to know there’s an IThingRepo and IStuffProvider and what they do - but I don’t consider their implementations to be. Storage details like which SQL dialect we’re using aren't the domain's concern. The domain encapsulates the important logic and delegates the mundane act of persistence to the repo.
  • As further backup for this, you should be able to swap out repo layers (e.g. for testing, perf, or infra changes), and you shouldn’t lose business logic if you do so. Some may regard this as theoretical, but I’ve done it various times in production systems (Dapper vs. EF, RDBMS vs. document vs. graph, etc.).
  • King-side-slide correctly notes that the rule may be needed in updates as well as adds, but then puts the rule in the repo's add method, which would leave a gap. This points out that in the repo, you’d indeed have to choose a place or duplicate logic (or contort things to call common code).
  • In this case the email field is in the same DB table as our aggregate root (User), but in many cases we may require coordination among multiple root types - and therefore multiple repos - and possibly other domain services. As a result, putting code like this into a specific repo doesn't work as a general solution.

So where to put it? I think this is a great case for a domain service, which lives in the domain but coordinates among multiple aggregate roots (which is the case here, since we need to vet an email address against all existing ones). Let’s say we start with this:

class EmailUniquenessService
{
    public function validateUniqueness($email)
    {
        if ($this->userProvider->emailExists($email)) {
            throw new UserAlreadyExistsException();
        }
    }
}

Same basic code you started with - in this case using your repo that's injected by interface - but now it's encapsulated in the domain, reliable and ready to use even if you rewrite your app and persistence layers.

But there's still a problem: your app layer can ignore it, grab a repo, and save away; maybe the dev who does the next rewrite doesn't even know the service exists. I like to have guarantees that if something gets saved, it must go through the domain. There are various ways to achieve this, depending on other rules, your architecture, and your language. Consider this instead:

class UserSaveService
{
    public function saveUser($user)
    {
        validateUniqueness($user->email);

        $this->userRepo->save($user);
    }

    private function validateUniqueness($email)
    {
        if ($this->userRepo->emailExists($email)) {
            throw new UserAlreadyExistsException();
        }
    }
}

That's better: it encapsulates the operation so each save is preceded by a uniqueness check (and no hidden chrono dependencies). To force its use, you could put the repo's save method on an interface that's internal to the domain and callers can't access (see edit); they'd have to inject this domain service instead.

This service, unlike the repo, is easily unit testable (just inject a mock for the repo). It can also be extended to transparently add further rules that your domain must enforce but that other layers needn't bother with unless a domain exception is thrown.

Also: you may want validate and save to run in a single transaction to prevent race conditions; this is a point in favor of king-side-slide's answer, since both could run in one query. However, I don't think it's worth scattering domain logic and running against my points above; use a unit-of-work (UoW) instead. (And if you're relying on the repo for enforcement, you can add a DB constraint and catch a SQL exception on collision.)

EDIT:

Struck the internal interface idea per Jordan's comment. In langs like C# that support the internal keyword, the following could work (I'd love to see a PHP way to require domain involvement):

Add a new class to the domain alongside User called VerifiedUser; make its constructor internal. Change your repo signature to accept a VerifiedUser, not a User, as its parameter; your domain is now the only layer capable of fulfilling the repo's Save() method contract. It could be as simple as a wrapper object that holds the real User.

I've never personally gone to this extent of guarantees in my own code (relying on knowing to call the domain service), but I'd certainly consider it - at least on larger, more confusing projects w/ many devs of varying skill/experience levels.

9
  • 1
    If the user repository interface with the save() method is internal to the domain, how can the interface be implemented from outside of the domain, in another layer? Commented Oct 9, 2020 at 12:35
  • @JordanWalker Good call. It was a spur-of-the-moment idea that I hadn't fleshed out, and I agree that it's likely infeasible (even w/ notions like C#'s friend assemblies, which was in the back of my head at the time). I added an alternate idea at the bottom; it carries some cognitive overhead and indirection, but that may be cheaper than a subtle validation bug in the app.
    – Prophasi
    Commented Oct 10, 2020 at 22:22
  • Fair enough. PHP doesn’t actually have an internal keyword btw, but it’s an interesting idea for languages that do. Commented Oct 11, 2020 at 8:04
  • True, and good to call that out. I was aware it doesn't, but I should've delineated better between PHP and language-agnostic DDD concepts (I found the question searching for the latter). Guaranteeing the domain call is a further level of refinement and not essential to the answer, but I'd love to see someone chime in with a way to do it in PHP.
    – Prophasi
    Commented Oct 11, 2020 at 14:26
  • Personally I think ensuring a User is unique before allowing it to be saved is the responsibility of an application service. Maybe my thinking is closer to Clean or Hexagonal architecture, but I don't think it's possible or even sensible to try pushing every single business rule down into the "domain model"... Commented Oct 11, 2020 at 17:40

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.