Yury Golikov
Yury Golikov

Reputation: 129

The aggregate must know and base its behavior only on its own state? Can the aggregate use the state of other aggregates in its behavior (methods)?

Can the aggregate use the state of other aggregates in its behavior (methods)? Should I inject links to other aggregates, services with access to other aggregates. Or the aggregate must know and base its behavior only on its own state?

Does the DDD community have a clear understanding on the "Aggregate" pattern on this issue? Or this question does not apply to DDD? On the other hand, for example, I've heard a fairly unambiguous view of the community that you do not need to inject a repository to the aggregate.

Where is the boundary between the fact that the operation of the meaning of the aggregate that precedes should not go to the level of services (anemic model), and meanwhile, to reduce the dependence and connectivity of aggregates?

If the aggregate is based only on its state and there are no external dependencies, then it is necessary to make a layer from the domain services? How to call these domain services?

I have an example. I deliberately threw out all the extra to simplify it as much as possible. And left only one operation. In this example, I implemented this approach: 1. Aggregates are independent and satisfy invariants only on its own state. 2. The logic belonging to the domain, but interacting with other aggregates and services is transferred to the domain services.

Example description: This is bounded context - auction. Here there are Lot, Bid, Bidder, Timer - which are put or restarted to count down the time before the victory. The operation that is being considered here is "make bid":

  1. Application layer get from client required data, get aggregates, services, repositories. Than call domain service (Domain Service BidMaker -> makeBid) and passes on to that service all that is necessary.
  2. Method of domain service (#Domain Service# BidMaker -> makeBid)

    а) Call method of aggregate (AR Lot -> makeBid), then this method check invariants, then make bid and add it to bidIds.

    b) Checks whether the timers exist in lot, if no start new timers or restarts old timers using the domain service - WinnerTimer.

    c) If timers new - save them using repository

    d) Call method of aggregate (AR Lot -> restartTimers), then this method add timers to winnerTimerId и winnerTimerBefore2HoursId.

I got a duplication of the name of the methods of the domain service and the aggregate. And logically, however, the "make a bid" operation belongs to the Lot aggregate. But then you need to transfer the logic from the BidMaker domain service to the aggregate, which means that you will also need to inject a timer repository and a timer service into the aggregate.

I would like to hear opinions - what would you change in this example and why? And also opinions on the first and main issue. Thanks to everyone.

/*Domain Service*/ BidMaker{
  void makeBid(
    WinnerTimer winnerTimer,
    TimerRepository timerRepository,
    int amount,
    Bidder bidder,
    Lot lot,
  ){
    //call lot.makeBid
    //check Lot's timers and put new or restart existing through WinnerTimer
    //save timers if they new through TimerRepository
    /call lot.restartTimers
  }
}

/*Domain Service*/ WinnerTimer{
  Timer putWinnerTimer(){}
  Timer putWinnerTimerBefore2Hours(){}
  void restartWinnerTimer(Timer timerForRestart){}
  void restartWinnerTimerBefore2Hours(Timer timerForRestart){}
}


/*AR*/ Lot{

  Lot{
    LotId id;
    BidId[] bidIds;
    TimerId winnerTimerId;
    TimerId winnerTimerBefore2HoursId;

    void makeBid(
      int amount,
      BidderId bidder,
      TimerId winnerTimerId,
      TimerId winnerTimerBefore2HoursId
    ){
      //check business invariants of #AR# within the boundaries
      //make Bid and add to bidIds
    }

    void restartTimers(TimerId winnerTimerId, TimerId winnerTimerBefore2Hours){
      //restart timers
    }
  }

  Bid{
    BidId id;
    int amount;
    BidderId bidderId;
  }

}

/*AR*/ Timer{
  Timer{
    TimerId id;
    Date: date;
  }
}

/*AR*/ Bidder{
  Bidder{
    BidderId: id;
  }
}

My english bad, sorry!

Upvotes: 2

Views: 433

Answers (1)

VoiceOfUnreason
VoiceOfUnreason

Reputation: 57249

the aggregate must know and base its behavior only on its own state?

Its own state, plus the arguments passed when the aggregate is told to change.

Because aggregates describe consistency boundaries, you shouldn't ever be trying to couple the current state of two separate aggregates together. But there's nothing wrong with updating this aggregate using a stale snapshot of another aggregate as an argument.

I won't normally use the aggregate root itself; I think a method with two aggregate roots is confusing, because it's not obvious which one is being changed. But a read-only view of an aggregate doesn't have that problem.

A better option might be to use a stateless domain service as an intermediary. So methods that change the state of Lot would never change the state of Timer, and would never touch the Timer directly, but the Lot would be able to ask questions about the state of a Timer, passing to the stateless service the id of the timer it care about.

Can you give arguments, why do you suggest to use domain service from Aggregate and not before to the aggregate method call?

Tell, don't ask is an argument in favor of passing the domain service to the aggregate, and allowing the aggregate to decide which id to pass to the service and under what circumstances. Following that pattern means keeping the logic that describes the relationships between the aggregate and the timer within the aggregate itself, rather than having that logic scattered through out the client code.

After all, part of the motivation for separating the domain model and the application is that you can maintain all of the domain logic in one place.

That's the approach that is consistent with the patterns described in the blue book.

A counter argument is that you are no longer treating the domain logic as a function -- it now has this read effect in the middle of it. There are advantages to keeping the effects out of the domain logic (you don't need to mock, so it's all easy to reason about and easy to test).

How do you practically use it?

Pretty much exactly as it says on the tin -- in general, it looks like an orchestration between two repositories; one repository provides the aggregate you are going to modify, the other provides the read only view that you are going to pass to the modified aggregate. The implementations of the repositories themselves are just plumbing.

The key idea being that, in a use case where we only expect to read an aggregate (not change its state), then we invoke a repository method that allows us to access a read only copy - as opposed to having a single repository that is invoked for all use cases (this is not taken from the blue book; it is a lesson that was learned in the years since).

Upvotes: 4

Related Questions