0

I realize that DDD advocates eventual consistency, thus allowing for a certain period of time in which the system might be inconsistent. Embracing eventual consistency, we can therefore model our aggregates in such a way that only 1 is updated per transaction.

However, my question is, what if the relationship between multiple aggregates is such that eventual consistency is either not acceptable, or compensating actions would be too complex to implement? Does that mean that DDD is not the right choice in that scenario, or does it mean that we simply cannot use the 1 aggregate per transaction rule? Or does this maybe mean that the multiple aggregates need to be merged into 1 greater aggregate?

To give a more concrete example, consider the following: An auction system, composed of Items, Auctions, Bids, Users and UserBalance. Users can place a bid on an auction if the following conditions are met:

  • They outbid the current highest bid, if any

  • Their balance has enough funds to place the bid

When a bid is outbid, the user that placed the bid should receive the funds used in placing that bid. Users can increase their UserBalance by other actions, such as purchasing, rewards, etc.

I envisioned the following aggregates: Auction composed of Bids, User and UserBalance.

Imagine Auction has a method called placeBid that handles the bid placement logic.

In order to preserve the 1 aggregate per transaction rule, Auction should accept a userBalance object, and check if the bid placement is valid. If yes, it then proceeds to create a bid, commit the transaction and publish a domain event. However, it can happen that a UserBalance no longer has sufficient funds to pay for a bid by the time the BidPlacedHandler is triggered. This means that other users would see the newly placed bid before the balance is updated. And this also means that once the UserBalance update fails, the current highest bid will need to be deleted, and the previous bid reinstated as the highest bid, unless of course another bid was placed in the meantime.

While handling these compensatory cases might be slightly unreadable, or difficult to grasp, my bigger concern is the fact that users might be able to see an invalid bid as the highest bid for an auction. Is there any guidelines as to how this problem would be approached, in this particular case, or in general?

  • Do you have a citation for the claim that "DDD advocates eventual consistency?" – Robert Harvey Nov 20 '18 at 15:40
  • I envisioned the following aggregates: Auction composed of Bids, User and UserBalance. -- None of those things are aggregates, except possibly Auctions. A User is an entity. A User Balance is a property of an account. – Robert Harvey Nov 20 '18 at 15:42
  • users might be able to see an invalid bid as the highest bid for an auction. -- Aren't bids essentially "not valid" until they are accepted? – Robert Harvey Nov 20 '18 at 15:44
  • 2
    Here's my point. Regardless of what Eric Evans, Martin Fowler or Vaughn Vernon says, ultimately the behavior of the system is going to be governed by the business rules at hand, not necessarily by some notion of "eventual consistency" or any other software maxims. Vaughn Vernon doesn't have to deal with auction dead-beats; the auction house does. Your job is to arrange your aggregates in such a way that you minimize immediate consistency across aggregates if you can, while still adhering to the auction house's business rules. – Robert Harvey Nov 20 '18 at 16:08
  • 1
    I conclude that you don't need the scalability that microservices and eventual consistency provides. – Robert Harvey Nov 20 '18 at 19:46
1

I want to begin this by first addressing the problem space here as I see a bit of misunderstanding regarding both "eventual consistency" and DDD.

DDD is a design process that understands real-world problems can sometimes reach a level of complexity that they cannot be cleanly separated and modeled into completely independent units. That is, process itself can have rules and when such a situation arises, eventual consistency can be employed to offer a solution. I don't read the quote you offer from the blue book (in the OP comments) as advocating for any mode of consistency, rather pointing out that eventual consistency is often a desirable and sometimes necessary attribute within a system of which we need to be aware when forming our expectations.

Whether or not a given system needs to employ an eventual consistent paradigm should be determined through analysis of its requirements in terms of scaling, not through analysis of it's design (though the former can certainly inform the latter). This decision should not be made lightly (and doesn't have to be all-or-nothing), because the trade-offs are steep and often require much greater complexity in terms of implementation where employed.

As to your system, I think you may have complected a couple of things together.

The first is that an Auction would be better understood/implemented as a Saga/ProcessManger rather than an Aggregate. That is, an Auction represents a grouping of related events that must be managed in terms of process. In this way, your Auction is given the responsibility to oversee it's own consistency and can therefore usefully abstract business requirements.

This flows into the second issue of not separating responsibilities into the correct entities. The issue you describe is predicated on your Auction needing to "ask" for an account balance. Clearly, it should be the responsibility of your Account (UserBalance is a confusing name) to keep track of the current balance. Therefore, it must also be responsible for creating a new Bid. In this way, we can cleanly separate the logic necessary for verifying an account balance from that of placing bids. Nobody at an auction cares about account balances, so let's keep that separate from our auction. Think of it like this, "how does an auction work?". It can be modeled relatively simply:

OfferBidHandler (moves money into escrow, creates Bid):

// throws NotFound
account = accounts.Find( cmd.UserId ) 

// throws InsuficientFunds or raises `BidOffered`
account.OfferBid( cmd.AuctionId, cmd.LotNumber, cmd.Amount ) 

PlaceBidHandler (keeps track of all Bids or maybe just highest Bid):

// throws NotFound
auctioneer = auctioneers.Find( cmd.AuctionId ) 

// raises Outbidded
auctioneer.ReceiveCompetingBid( cmd.UserId, cmd.LotNumber, cmd.Amount ) 

RevokeBidHandler (removes money from escrow):

// throws NotFound
account = accounts.Find( cmd.UserId ) 

account.RevokeBid( cmd.AuctionId, cmd.LotNumber ) 

The Auction (which mediates the process) is simply responsible for reacting to BidOffered events in a way to trigger PlaceBid OR RevokeBid commands (should NotFound be thrown), and reacting to Outbidded events in a way to trigger RevokeBid commands.

If you would like to keep Account and Auction in separate contexts (you don't want Account.OfferBid), this can be modified further to add an extra layer along the lines of: PlaceOrder -> PaymentReceived -> PlaceBid (in this case ordering a Bid and debiting account) and BidRevoked -> RefundPayment to cleanly separate accounts from auctions.

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

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