Pergunta

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?

Foi útil?

Solução

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.

Licenciado em: CC-BY-SA com atribuição
scroll top