TransWikia.com

Exceptions in DDD

Software Engineering Asked by yadiv on December 18, 2020

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?

3 Answers

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.

Answered by Prophasi on December 18, 2020

You can impose validation in every layer of your application. Where to impose which rules depend on your application.

For example, entities implement methods that represent business rules while use-cases implement rules specific to your application.

If your building an e-mailservice like Gmail one could argue that the rule "users should have a unique e-mailadress" is a business rule.

If your building a registration process for a new CRM system this rule is likely best implemented in a use-case as this rule is also likely to be part of your use-case. In addition, it might also be easier to test as you can easily stub the repository calls in your use-case tests.

For aditional security, you could also enforce this rule it in your repository.

Answered by thenoob on December 18, 2020

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 a EmailMustBeUnqiueRule 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 EmailMustBeUnqiueRule will be applied on the creation of a User, but what about UserIsInGoodStandingRule?. Slowly but surely, the grainularization 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: what ever 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.

Answered by king-side-slide on December 18, 2020

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP