Search code examples
.netentity-frameworkmicroservicesdomain-driven-designaggregateroot

Modeling a many to many relationship to a DDD aggregate


Trying to understand how this can be modeled without breaking DDD rules.

This is a system for managing and sending Gift Cards to Customers

Customer
-------
CustomerId (Primary Key)
CustomerType
FirstName
LastName

GiftCard
------
GiftcardId (Primary Key)
ShopName
Suspended
CreatedDate

CustomerGift
-------------
CustomerId (Foreign Key -> Customer)
GiftcardId (Foreign Key -> Giftcard)
GiftDate

I have the following models

public class Customer
{
  Int CustomerID;
  String FirstName;
  String LastName;
  String CustomerType; 
  IEnumerable<CustomerGift> CustomerGifts;
}

public class CustomerGift
{
    Int GiftCardID;
    Int CustomerId;
    DateTime GiftDate;
}

public class GiftCard
{
    Int GiftCardID;
    String ShopName;
    Bool Suspended
    DateTime CreatedDate;
}

Domain rules:

  • GiftCards are bulk entered into the system and can be assigned at anytime
  • A GiftCard can be suspended at anytime
  • A GiftCard can be sent upto 50 customers
  • A CustomerGift can be deleted at any time
  • A Customer can be deleted at any time
  • A Customer can receive upto N number of GiftCards depending on the customer type

To my understanding the Customer and GiftCards are aggrigate roots and the CustomerGift is a child.

However at its current state there is no way to enforce the "GiftCard can be sent upto 50 customers" rule without exposing the CustomerGift to the GiftCard. Which would break the rule by sharing a child entity.


Solution

  • In general, when modeling a domain, it's best to forget about what the tables in a relational schema would look like.

    Your Gift Card aggregate can track which customer holds a given gift card (assuming that a given gift card can only be held by at most one customer at a time; if it's at least zero customers at a given time, then this becomes a set). Your Customer aggregate can track how many and which gift cards a given customer holds (as it does).

    This suggests that the process of assigning a gift card to a customer progresses like:

    • reserve one of the 50 "slots" in the customer (can fail)
    • attempt to associate gift card with customer (can fail)
    • complete reservation (at this point, the gift card is redeemable)

    You'd then have (whether or not you had the process cancel a reservation if association failed) a periodic reconciliation: check for reservations of some age and if they're associated, complete them and otherwise cancel them.

    As you may note, this isn't atomic. The real world isn't atomic either and we're capturing reality.

    At least one of suspension or spending a gift card is probably going to be a process like the assignment process. It's probably advisable to make the more exceptional use case a multi-step process: if you expect more gift cards to be spent than suspended, then you can make suspension a process like

    • mark a gift card in the customer's wallet as to-be-suspended
    • suspend the gift card
    • mark the gift card in the wallet as suspended (or remove it?)

    and then spending the gift card can be done just through the customer aggregate (double-spend prevented by the customer) with a later reconciliation process updating the gift card as spent. Alternatively, you can make suspension an operation that only touches the gift card aggregate and spending requires a pas de deux (mark customer's wallet with intend-to-spend, spend gift card, mark as spent).