DDD inject repository on domain service VS orchestrate flow on application service
Asked Answered
U

1

6

I am currently working with DDD and I have a question about application services VS domain services VS repositories interfaces

As I know:

  • Application services are used for handle the flow of use cases, including any additional concerns needed on top of the domain's.

  • Domain services are used for encapsulate such behaviors that do not fit in a single domain object.

So, taking account into, for example, this use case:

"when you create a new Car entity(uuid,name) in the system, the car name must be unique (no more cars exist with this name) or the car name can't contains another car name from database as substring",for example, just an example of use case that forces me to take a look into other entities in the repository when I am creating an object

So the question is: Where should I do the checks and/or inject the repository Interface?

- Opt 1) In the application service, injecting the RepositoryCarInterface, do the checks and save the Car:

class CreateCarApplicationService
{
    private carRepositoryInterface $carRepository;

    public function __construct(CarRepositoryInterface $carRepository)
    {
        $this->carRepository = $carRepository;
    }

    public function __invoke(string $carUuid, string $carName): void
    {
        $this->ensureCarNameIsUnique($CarName);
        $car = new Car($carUuid,$carName);
        $this->carRepository->save($car);
    }

    private function ensureCarNameIsUnique(string $carName): void
    {
        $CarSameName = $this->carRepository->findOneByCriteria(['name' => $carName]);
        if ($carSameName) {
            throw ExceptionCarExists();
        }
    }
}

- Opt 2) Create this logic into a domain service (with the purpose of keep the domain logic near to the domain objects) and invoke it from a more simple application service which has the final responsibility of saving the model interacting with database:

class CreateCarDomainService
{
    private carRepositoryInterface $carRepository;


    public function __construct(carRepositoryInterface $carRepository)
    {
        $this->carRepository = $carRepository;
    }

    public function __invoke(string $carUuid, string $carName): Car
    {
        $this->ensureCarNameIsUnique($CarName);
        return new Car($carUuid,$carName);
    }

    private function ensureCarNameIsUnique(string $carName): void
    {
        $CarSameName = $this->carRepository->findOneByCriteria(['name' => $carName]);
        if ($carSameName) {
            throw ExceptionCarExists();
        }
    }
}
class CreateCarApplicationService
{
    private carRepositoryInterface $carRepository;
    private CreateCarDomainService $createCarDomainService;

    public function __construct(CarRepositoryInterface $carRepository)
    {
        $this->carRepository = $carRepository;
        $this->createCarDomainService = new CreateCarDomainService($carRepository)
    }

    public function __invoke(string $carUuid, string $carName): void
    {
        $car = $this->createCarDomainService($carUuid,$carName);
        $this->carRepository->save($car);
    }

}

I am not very sure about the fact of injecting repository interfaces into domain services, because of as Evans said:

A good SERVICE has three characteristics:

-The operation relates to a domain concept that is not a natural part of an entity or value object

-The interface is defined in terms of other elements of the domain model

-The operation is stateless

But I want to push my domain logic as deep as I cant

And, as I read in other StackOverflow posts, inject repository in domain object is not allowed/recommended:

Do you inject a Repository into Domain Objects?

Should domain objects have dependencies injected into them?

Unspent answered 8/8, 2021 at 18:27 Comment(0)
C
2

Option 1

The ideal case is that repositories are only used by your orchestration (application) layer and have absolutely nothing to do with your domain model (domain layer). Thus, your repo would be injected into your orchestrator, not into your domain model (option 1).

In your case, you have an orchestration layer that

  • has a cars repository injected into it
  • loads names of cars from the repository
  • uses DDD to validate the name of the new car is not in the names of existing cars, etc.
  • if yes: create the car in the domain; if no: fail domain validation
  • uses the repo to persist the change in state on the domain (in this case, saving the new car using the repo)
  • returns results (if a request/reply scenario)

There's a small problem with this though. You could argue that it would be more efficient to take the name of the car and pass it to a query against the repo to see if the name is unique. That's true, but the trade off is that some of your domain logic (checking for uniqueness) has been moved out of the domain to the repo and orchestration layers.

So, consider carefully which you'd prefer.

Option 1, Scenario 1: DDD as much as possible

// inefficient, but we're done with the repo immediately
var carNames = repo.GetCarNames();
// all the following calls are on our domain, easily testable
var carCreator = new CarCreator(names);
var carCreationResult = carCreator.TryCreateCar(carNames, newCar);
if (carCreationResult.Failed) return carCreationResult.Errors;
// finally save and return
repo.Save(carCreationResult.Car);
return carCreationResult.Car;

In the above, TryCreateCar could be implemented as a simple check against a dictionary inside carCreator -- totally within the domain, testable, and not relying on the repo.

Option 1, Scenario 2: Be efficient

// uniqueness check requires repo; mixes in domain concept of uniqueness with a repo query
var canCreateCar = repo.IsCarUnique(newCar.Name)
if (!canCreateCar) return error;
// creation separated from uniqueness check; wouldn't have to check uniqueness in TryCreateCar (it was checked above)
var carCreator = new CarCreator(newCar);
var carCreationResult = carCreator.TryCreateCar(carNames, newCar);
if (carCreationResult.Failed) return carCreationResult.Error;
// finally save and return
repo.Save(carCreationResult.Car);
return carCreationResult.Car;

That IsCarUnique method on the repo is hiding some domain logic though!

Option 2

We'll dismiss this option because we simply don't want a non-domain concern to be a dependency of our domain model. That's the sum-total of the reason to avoid this. When you take a non-domain concern and make it a dependency, your domain model becomes harder to test.

Worse, I've seen code that interleave concerns. Imagine the case of an orchestration layer that gets some entities via a repo, makes some changes to the domain, saves some entities, loads some more, injects the repo into the domain so it can use the repo to load some more, and then finally save. That's an untestable and hard to read/maintain mess!

In Summary

Option 1 Scenario 1 allows us to keep all of our domain concerns together and encapsulated. This is well worth it. If the rules change, we only have to modify our domain model's data and behavior, leaving the orchestration intact.

Cornetist answered 10/8, 2021 at 21:20 Comment(1)
Hi Kit, First of all thanks for your complete answer, you had made it easy to read and easy to understand step by step. I am going to code your option 1 /scenario 1, even a little bit more inefficient, the number of rows in the real model are not a lot, so, I prefer keep the domain logic together into a domain service; "CarCreator" in the example. ThanksUnspent

© 2022 - 2024 — McMap. All rights reserved.