-1

I have Buyer, Product, Seller, and Offer. The buyer offers to buy a product and the seller either accepts the offer or declines it. I also have the following invariants:

Buyer can't make an offer for a product if there is a pending one already. Seller can't accept a non-existing offer from a buyer. I created the following aggregates Buyer, Product, Seller, ProductOffers. ProductOffers contained all offers from all users. ProductOffers also have a method TryMoveOfferToAccepted() which is called when OfferCreatedEvent is raised from the buyer. TryMoveOfferToAccepted() then raises event triedMoveOfferToAccepted which is handled by the Product aggregate which checks whether the product can be bought (checks for sufficient quantity...) and the if successful raises an event ProductBought event which then is handled by ProductOffers (moves the offer from pending to accepted).

Is this a good way to do this? And how can I be sure that someone doesn't call Product's aggregate buy method without first checking whether an offer from buyer exists?

manfrom
  • 91
  • 7
  • Why a `ProductOffers` AR if there can only be one active offer at a time and why don't you immediately reserve inventory for an ongoing offer to reduce/eliminate? the risk of missing inventory on accepted offers? As for making sure a command only happens if X happened then sometimes I model the command as `notifyXHappened(event)` instead of a standalone command e.g. `product.notifyProductBought(event)` instead of `product.buy()` which better communicates the coupling. – plalx Mar 23 '22 at 00:36
  • Anyway, to me perhaps the `ActiveProductOffer` could be part of the `Product` AR while the history would live outside which allows for strong consistency here. – plalx Mar 23 '22 at 00:39
  • I don't reserve inventory because buyer makes an offer and his offer can be rejected. Product can have many offers from many different buyers – manfrom Mar 23 '22 at 08:21
  • Nevermind, I read this as if it was a sell order. Well for the uniqueness set validation you could always just use a DB unique constraint if you dont have a distributed system which is more pragmatic than a collection. If not you could consider a collection like `BuyerOffers` instead of `ProductOffers` depending on which one tends to be larger. – plalx Mar 23 '22 at 12:24

2 Answers2

1

You are over-engineering your problem.

Get back to your use cases:

  • A buyer makes an offer to buy a product
  • A seller accepts an offer

You could model that with a single aggregate, Product (root) + Offer (object value or entity). The name ProductOffer should be a good hint that this object should not be a root "aggregate" and is aggrgated to the Product. Also note that single entities are not aggregate, they are entities. The term aggregate describes situations with multiple object.

The seller can use Product.MakeOffer() to make an offer. This will check there is no existing pending offer from that buyer in the Product.Offers collection.

The buyer can use Offer.Accept() to accept an offer. Since the offer is a child item in the aggregate, you need to return the whole aggregate from the repository, allowing you to compare the Offer.Quantity to the Product.InStock for instance.

Addendum:

For rejection, you don't need that complexity. Aggregates MAY overlap into a polysemic model. You could have a separate Offer entity (not the same used by the Product+Offer aggregate) used for the offer rejection use case. This could save some database read performance.

ArwynFr
  • 1,383
  • 7
  • 13
  • What if the offers for a certain product are too much and I don't want to load them all into memory? – manfrom Mar 23 '22 at 11:10
  • Can I for example create aggregate UserProductOffer that will contain all offer for a product from a single user? How would this looks like? – manfrom Mar 23 '22 at 11:11
  • UserProductOffer is not an aggregate as there is no root entity. Also it would not allow you to enforce any business rule spanning the whole product. – ArwynFr Mar 23 '22 at 11:16
  • You don't need to read all Product+Offers properties from your database, only those useful for rule validation. This can make your business object quite lean, even if there is millions of them. – ArwynFr Mar 23 '22 at 11:17
  • If you still have too much data for that, you will need to switch to eventual consistency. Check offer can be accepted without product rules validation, raise OfferAcceptingEvent, have the product check whether the stock allows, raise OfferAcceptedEvent or OfferRejectedEvent, and update offer. – ArwynFr Mar 23 '22 at 11:20
  • How would the UI look like? If I accept an offer without having sufficient quantity in product the offer in the UI will be displayed as accepted but when I reload it will change? – manfrom Mar 23 '22 at 12:44
  • You should not mark the offer accepted in UI when the OfferAcceptingEvent occurs, but when the OfferAcceptedEvent occurs. You will need an asynchronous UI pattern, marking the offer as "in process" at first, and then notifying UI whether accepted or rejected. – ArwynFr Mar 23 '22 at 13:00
  • When I accept an offer it should be moved to accepted. I move the offer to accepted and if OfferRejectedEvent occurs I move it from accepted to rejected collection? – manfrom Mar 23 '22 at 14:52
  • With eventual consistency, when user clicks on "accept", it does not accept the offer. Offer is moved to **accepting**, then you check and update stock count, then you move the offer to **accepted** or **rejected** depending on if there was enough stock or not. – ArwynFr Mar 23 '22 at 15:17
-1

Don’t forget about simplicity. Simplify everything you can. The fewer things you have, the easier it’s to control them. My friend from https://transformagency.com/ has the same philosophy, which I really like. What to make things more complicated? When you can make them more simple. Minimalism is a new must. As you can see, it’s everywhere. We often notice it in design, food, and general lifestyle. So, I think you’ve solved your issue successfully. If any problems happen, let us know.

  • 1
    As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Jul 06 '22 at 12:07