Search code examples
microservicesdomain-driven-design

what to do when strong consistency is required, but entity (might) need to be shared across boundary


I'm currently developing an infrastructure as a service application. I define 3 bounded context Billing, Cloud and Hosting. There's a wallet entity (or aggregate how should I define it?) which may appear in several bounded context. Billing is interested in transaction_date, invoice_id while Cloud and Hosting are interested in balance (whether a user has enough balance to buy a service).

enter image description here

So since each bounded context interested in difference fields here's my first design. I use upstream/downstream pattern to replicate wallet entity from Billing into Cloud and Hosting using event message (eventual consistency). Btw there's a case where a user buys hosting and cloud at the same time. so both cloud service and hosting service check if wallet has enough balance using the replicated data. Let's say that wallet doesn't have enough balance to buy both cloud and hosting at the same time. so now race condition occurs. Domain expert says that race condition like this should never occur. So how do I properly design bounded context/context map in this case.

enter image description here

Edit: I'm also thinking of merge the Cloud and Hosting context together and name it Infrastructure context, but that would be hard to scale.


Solution

  • If the domain expert says a race condition like this should not occur, they are by implication saying that if the cloud/hosting contexts cannot be sure that there are funds in the wallet they should reject the sale. The only context which can have a strongly consistent view of a wallet's balance is the billing context.

    The cloud and infrastructure contexts don't really care about the balance of the wallet. They care about the balance available to them.

    This suggests that a reservation pattern is applicable:

    • someone wants to spend X units from wallet Y in context Z
    • context Z checks if the balance of wallet Y available to it is greater than X
    • if it is, it deducts X from its view of the balance of wallet Y available to it and we're done (presumably something will later generate a billing event)
    • if it's not, it communicates a desire to reserve enough (A) of wallet Y to the billing context; it might reject the spend attempt or just wait... at some point that someone will retry the spend attempt (presumably there's a reason they wanted to spend...)
    • the billing service checks if there's enough unreserved balance in wallet Y to satisfy the reservation
    • if there isn't, nothing needs to happen
    • if there is, an event saying that A units of wallet Y is now reserved for context Z is published

    You can elaborate on this with eventual unreservations (in which case, to prevent double-spend, you want the source of truth for unreservations in context Z to be context Z: the failure mode must always be "lose the sale").