Domain-Driven Design – Does Reference by ID Between Aggregates Lead to an Anemic Domain Model?

domain-driven-designdomain-model

Overview

I'm trying to wrap my head around the best-practice way to link Aggregate Roots together without transferring a lot of the business logic from the Entities/AR's themselves to Services, all while still adhering to the Vaughn Vernon tip to:

Prefer references to external Aggregates only by their globally unique identity, not by holding a direct object reference (or “pointer”) …..

Details

Let's take for example a simple Search System. This Search System allows users to perform a standalone Search for a Customer against a suspicious persons list.

It also allows to perform Batch Searches which is merely a collection of Searches against a list of Customers

I've modelled the above like so.

enter image description here

In essence the

  • Search
  • Batch Search

are both Aggregate Roots. A Search can run standalone but it can also run as part of a Batch Search, in which case the created Batch Search will contain the Searches.

Coding this is fairly straightforward

class BatchSearch {
  constructor(customers) {
    this.searches = []
    this.customers = customers
  }

  run() {
    for (customer of this.customers) {
      const search = new Search(customer)
      search.run()
      this.searches.push(search)
    }

    this.markAsCompleted()
  }

  ...
}

However, Vaughn Vernon states that holding a direct reference between Aggregate Roots is bad design.

From, Effective Aggregate Design Part II: Making Aggregates Work Together:

Prefer references to external Aggregates only by their globally unique identity, not by holding a direct object reference (or “pointer”) …..
Use a Repository or Domain Service (7) to look up dependent objects ahead of invoking the Aggregate behavior

As I understand it, he advocates moving interactions betweens Aggregates in Services like so:

class BatchSearchService {
  constructor() {
  }

  createBatchSearch(customers) {
    let searches = []
    const batchSearch = new BatchSearch()

    for (customer of customers) {
      const search = new Search(customer)
      // We link this Search with this Batch Search by ID only, here
      search.setBatchSearchId(batchSearch.getId())
      search.run()
      searches.push(search)
    }

    batchSearch.markAsCompleted()

    batchSearchRepo.save(batchSearch)
    searchRepo.save(searches)
  }
}

Doesn't this recommendation invariably lead towards an Anemic Domain Model?

AFAIK OOP is fundamentally the coupling of data and operations in a Class, but from what I understand in this scenario the operations are moved from the Batch Search Class to the Batch Search Service instead, leaving the Batch Search Class to only hold data

Best Answer

Vaughn Vernon's recommendation is one of the best tactical rules that one should follow. If you really need to hold a reference to another aggregate root then you need to review your aggregate's boundary as most probably they are wrong.

In this case the model is anemic because the business is anemic, it does not have any invariants that it needs to protect, at least from what you've presented. In any case, aggregates should be used on the write/command side of your architecture; I'm specifying this because, in almost all domains, "search" is a read/query side operation, but I suppose that your domain is a special one where a "search" implies some state mutation.