Domain-Driven Design – DDD: Put Logic in Service or Aggregate Root?

domain-driven-design

Let's say we are building a document management system.

One project has many documents. I decide to make projects as aggregate root.

If the logic to add one document into project is complicated, I can put it in aggregate root like:

class Project
{
    public function addDocument($params){}
}

But I can also make this just a domain service

class ProjectService
{

    public function addDocument($project, $params){}

}

What's the difference?

How to decide which one to use?

Thanks!

Best Answer

Project.addDocument is the right approach.

The guiding principle is the definition of an aggregate.

AGGREGATE A cluster of associated objects that are treated as a unit for the purpose of data changes. External references are restricted to one member of the aggregate, designated as the root. A set of consistency rules applies within the aggregate's boundaries.

In other words, if you have a collection of documents, then all of the consistency rules for the collection of documents belong with the collection, all contained within one aggregate root.

Putting the consistency rules into a domain service tends to lead to an anemic domain model, an anti-pattern.

In , all the work of changing state belongs in the aggregate; stateless domain services provide query support to the aggregate that is considering a change.

Warning: contrived example ahead

For instance, if you were trying to implement the invariant that a project isn't allowed to include more than $20 worth of documents, then you might see a signature like

class Project
{
    public function addDocument($document, $documentPricingService){}
}

The pricing service would know how to calculate the price of a document, or even a collection of documents, but it wouldn't know anything about the rule that the total needs to be below $20. The project would know how to pass the (updated) document collection to the domain service for pricing, but not anything about how pricing is done.

Typically, the application component is responsible for finding the pricing service before running the Project.addDocument command.