Search code examples
phpcqrsevent-sourcingaggregateroot

Event sourcing : how to convert an aggregateRoot into another one


Basically, the question is :

how to correctly build an event storage for an event sourced system that should be able to :

  • convert an aggregate into another one,

  • keep the same Id,

  • and still be able to reconstitute it from the event stream?

Now my example:

i have a ProspectiveCustomer that can be converted to PayingCustomer like this :

ProspectiveCustomer::convertToPayingCustomer(ProspectiveCustomerId $id)

The PayingCustomer would keep the same Id so its lifetime can be followed up.

So now imagine the following event Stream :

  1. ProspectiveCustomer was added to the CRM
  2. ProspectiveCustomer was made an offer
  3. ProspectiveCustomer accepted the offer and therefore was converted to PayingCustomer
  4. PayingCustomer paid its bill

Let's focus on point 4) :

We'll have a commandHandler that recieves a paymentCommand {customerId:"123", amount:"500€"}. Its job would be to :

  1. reconstitute PayingCustomer from its events'history
  2. call PayingCustomer::pay(Money $amount)

My question is about 1) reconstituting from History :

The EventStorage service would :

  1. look up for the AggregateId
  2. loads the events (SELECT * FROM Events WHERE ID = 'xxx')

The events'stack would now contain :

  • ProspectiveCustomerWasAdded
  • ProspectiveCustomerWasMadeAnOffer
  • ProspectiveCustomerAcceptedTheOffer

How could the commandHandler process PayingCustomer::reconstituteFromHistory(EventsHistory $events) while the $events are events issued from / applyable to the ProspectiveCustomer

EDIT

currently i'm handling the problem with PayingCustomer having its own Id, but holding a reference to the ProspectiveCustomerId.

But considering that :

  1. this is the same bounded context,
  2. the very same customer's lifecycle (ProspectiveCustomer ends when PayingCustomer starts),

it kind of feels messy because the model is now polluted by 2 Ids whereas one should be enough.

If it wouldn't be an event-sourced system, I'd definitly go for one unique Id.

That being said, and considering Event-sourcing is just an implementation detail, i'm looking for a way to have both aggregates keeping the same Id.


Solution

  • You have two aggregates but there is no "conversion". You step on a dangerous road, which can lead you to converting shopping carts to orders (for example).

    You already have two different concepts - prospective customer and paying customer. They were probably identified during your conversations with domain experts. This clearly means two aggregates, sometimes two bounded contexts. You should not do any conversion, but you can definitely create new aggregate reacting on something that happens in your system (order accepted).

    1. Prospective customer created
    2. Offer accepted
    3. Paying customer created from prospective customer
    4. Prospective customer removed (or marked as "converted", or deactivated)

    I would also expect that the term "conversion" came to you from domain experts. This is normal, since in Sales they use this terminology to indicate that someone who was interested actually made a purchase. They indeed call it "conversion" and you would be right including it to your ubiquitous language by using "3. Prospective customer converted" but this has nothing to do with technical conversion, meaning changing the object type.

    You need domain event handlers that would do (3) and (4) since you're saying this is the same bounded context.

    Aggregate unique identity generation is not a function of the aggregate itself, it is done outside and the aggregates gets its identity when being created as a factory method or/and constructor parameter. Therefore, when you create paying customer from a prospective customer, nothing stops you from using the same identity.

    However, you start to have assumptions that you always expect to have a prospective customer in order to retrieve your history or something else, using the same identity. Since this assumption is implicit, it is easily forgotten and in general especially discouraged in DDD (remember making implicit things explicit). You can easily keep the id reference to the prospective customer in your new paying customer and then you will be perfectly fine.