6

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

  • 2
    A Anemic Model isn't necessarily a bad thing. – T. Sar Aug 21 '17 at 12:37
  • 2
    I think the main business operation is Search.run(), which continues to exist after the creation of your BatchSearchService. Therefore this technique didn't lead to ADM as you said. It seemed so because, as you said, the batch search is only a group of searches, therefore it doesn't contain much business logic. As I understood, the service proposed by DDD author's is to solve dependencies only, not to contain the logic itself; if the logic was moved (which I think it wasn't in your code) then it's not following the idea correctly. – Emerson Cardoso Aug 23 '17 at 14:17
  • @EmersonCardoso service proposed by DDD author's is to solve dependencies only - I think that's the key sentence I've missed – Nik Kyriakides Aug 24 '17 at 9:01
8
+50

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.

  • So what's the suggestion from your end? Move the search to the Service or keep it in the Class? – Nik Kyriakides Aug 21 '17 at 8:51
  • @NicholasKyriakides If the a search only queries the system then it should not be an aggregate. – Constantin Galbenu Aug 21 '17 at 8:52
  • Pardon me, I didn't fully describe the whole scenario. A search can also be reviewed/confirmed if it's an actual/real hit. It has state itself and it's not something that only queries the system – Nik Kyriakides Aug 21 '17 at 8:54
  • @NicholasKyriakides I understand, that's why I added I suppose that your domain is a special one where a "search" implies some state mutation. My recommendation is that you follow Vaughn Vernon's recommendation. – Constantin Galbenu Aug 21 '17 at 8:56
  • 1
    Then don't apply the Aggregate tactical DDD pattern if the task is too simple, use CRUD. – Constantin Galbenu Aug 22 '17 at 18:13
2

I don't think you actually have two aggregate roots here. An aggregate root is an entity - which means it has "continuity and identity" on the domain level. From your description, comments, and from the code samples, it looks like only the Search type is actually an entity. BatchSearch itself seems to lack such an identity (at least, no such ID appears in the first version of the code), which probably makes it a (DDD) service, especially if it doesn't have externally visible state. It could also be treated as a value object, depending on how you use it. Now, if it is a service, note that this doesn't mean that you should move the functionality to some utility method; services can be implemented as objects (and be polymorphic, have dependencies, etc).

Vaughn Vernon's points are about loading and modifying the state of the aggregates. An aggregate is a group of related entities and value objects that has one externally visible entity (the aggregate root - essentially a Facade), which serves two purposes: (1) it defines the rules of traversal, and hides complexity, and (2) it provides some guarantees about the consistency of the aggregate as a whole when it undergoes modification. In the article you linked to, the issue revolves about the idea that two associated aggregates should not be modified in a single atomic operation, as this has certain design and performance implications (assuming atomic consistency is the goal).

So to avoid this situation, he recommends referencing the other aggregate (which, again, is an entity) by its domain ID. This doesn't entirely prevent the problematic scenario, but it does make it more difficult to realize, while still allowing navigation to the other aggregate. There's also the added benefit of reduced memory footprint, in the sense that the two aggregates are loaded independently.

In the Model Navigation section he talks about how you can use a form of lazy loading to support navigation between aggregate roots, and proceeds to recommend the "use a repository or domain service to look up dependent objects ahead of invoking the aggregate behavior", as opposed to looking up dependencies via a repository from inside an aggregate, which is really just an application of the dependency inversion principle (where this "repository or domain service" is used to manually perform dependency injection).

He further says that if it turns out that this approach simply doesn't work, then you should consider designing your system for eventual consistency instead (that is, the system should not assume that a successful update of one aggregate means that a related aggregate was also modified accordingly, and is in a valid state at that point in time), and then describes an event based approach to building such a system.

Now, the question is, does any of this apply to your BatchSearch class? Does it have a non-transient, domain-level identity? Is it better modeled as a service that constructs and returns a bunch of domain entities (Search instances)? Also, the article itself points out that there are cases where you may decide to break the rules, and lists several reasons for doing so, which is also something to consider.

  • Yes Batch Search does have an identity, since the user should be able to view a list of Batch Searches which he can manipulate (view info about it, start date, completion date, mark it as reviewed etc...) – Nik Kyriakides Aug 26 '17 at 20:52
1

One way to unify the model would be to use Search as the aggregate root with Invocation model as part of the aggregate.

It is not different from how you have already modelled, but thinking of Search and BatchSearch as two different aggregate models needs to go away.

Store Search as a resource with flags that represent the state as batched or not.

class Search {
    List<Invocation> invocationList;
    boolean: batched; 

    // you can check size of the list to determine whether it's batched or not - this simplifies the model further
}

class Invocation {
    List<SearchParameters> searchParameterList;

    ..
    //add more fields as required
    .. 
}

Design your use cases and model them around treating each invocation as a Search with a different number of invocations(Depending on the use cases you might name "Invocation" differently).

Thus models would be staying within the boundary of the aggregate root in the given context.

Hope it helps. Let me know if I need to account for any missed design concern.

  • I like this. A batch search is essentially another way of invoking a search with different parameters. To me these are simply two different business methods inside the same aggregate: SearchAggregateRoot – iberodev May 19 at 11:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.