7

I'm trying to learn about event sourcing for potential application to a booking system I am building - I think I understand the core concepts, but I'm having trouble understanding how to handle cases where updates to multiple aggregates / streams are required.

In my case I have modeled two aggregates - Events, which track the number of available seats, and Bookings for one or more seats at a single event which record details about the attendees. Everything I have read indicates that a transaction should update only a single aggregate, however when saving a Booking I need to update both the Event to track the remaining seats at the event, and the Booking to record changes to the attendee details. This creates the potential for inconsistency:

  • If I update the Booking before the event I need to handle the case where the update to the Event fails because the seats are no longer available (this happens even if I add a "Reserve your seats for the next 10 minutes" step, because at some point I need to update the booking details and confirm the seats).
  • Conversely if I update the Event first I need to handle cases where someone has managed to concurrently modify the Booking and there is a conflict.
  • In both cases an unexpected error will leave the Booking and Event inconsistent and I'm unsure how I could handle this.

I've tried looking into how similar problems in other domains are handled (e.g. transferring money between accounts), but I'm not 100% clear as to how those strategies ensure "eventual consistency", and so I'm not really sure how to apply this in my case.

How should updates to multiple linked aggregates be handled?

  • 1
    Are your aggregates both roots? Seems like they could be. Maybe this discussion about using a Saga will help: programmers.stackexchange.com/a/300695/63202. – Erik Eidt Dec 3 '15 at 0:31
  • @ErikEidt Yes? (I think so, still figuring out the terminology), So a Saga is an Aggregate Root to track updating multiple other aggregate roots, e.g. in my linked money transfer case the MoneyTransfer Aggregate is a Saga and in my case I could have a BookingUpdate aggregate to track the update to the Event and the Booking? That does make more sense now, although I'm still a little unclear as to how the Saga handles potential conflicts when updating other Aggregates. – Justin Dec 3 '15 at 0:40
  • If the "reserve your seats" option is available, I'd attempt a two-factor version of that: Firstly, before accepting their money, attempt to reserve their booking/seats with a marker that says they haven't paid yet. Then read back these details and confirm they've got the seats/booking without clashes/errors (if not either retry or return to ask to choose details again). Now you know they've got these if the credit check, etc succeeds. If it does, just update the marker to paid. Otherwise, cancel these seats/booking. – Mark Hurd Dec 9 '15 at 4:53
4

How should updates to multiple linked aggregates be handled?

Updates to multiple aggregates should each be applied in separate transactions. If this results in an unacceptable situation for the business, then the conflict should be detected and invoke the remediation policy.

It is often the case that over booking is allowed by the business (in cargo shipping examples, you will often see that there is a deliberate policy in place for booking beyond capacity).

In this case, that would probably mean that you change the bookings in an transactionally consistent way. The process manager, listing to the booking events, would then command the event to update. The event aggregate would accept the increased number of reserved seats, and fire two events - one announcing its current count of seats, and another announcing that the capacity of the event had been exceeded. Another process manager listens for the capacity exceeded event, and starts the mitigation process.

On the other hand, if the business invariant really is an absolute - if the business really can't allow more bookings than there is space at the event - if eventual consistency isn't good enough, then you have to accept the fact that you've got a business invariant that requires data from two different entities, and you need to find a model that incorporates those entities into the same aggregate.

  • If the invariant is an absolute, that simply means that during the booking you 'lock' the seats being booked BEFORE doing anything else. A lot of fixed-seat venues would work this way. This is the opposite case of overbooking as you might reserve seats for a customer that doesn't confirm his booking information during the process or who's creditcard bounces. Mitigation for this strategy is simply to open up the seats again when a booking fails, which means that an event might be sold out but at a later point still has seats opening up because bookings are not finalized. – JDT Mar 10 '16 at 10:38
4

Everything I have read indicates that a transaction should update only a single aggregate...

That's correct. And the best solution would be to follow that advice and refactor your aggregate at the transactional boundary: use a single aggregate.

If you think about it, the number of available seats is:

remainingSeats = maxSeats - seatsPurchased

So if you have a sequence of events like this:

{
    version: 1,        
    type : "EventCreated",
    maxSeats : 500
}

{
    version: 2,
    type : "BookingCreated",
    seats : 4,
}

The aggregate can be reconstructed into a object that looks like this:

{
    version : 2,
    remainingSeats : 496,
}

Meaning, you don't need to store the number of remaining seats. Instead, you can derive the value from the bookings. That would get you the single aggregate with a single transaction.

To attempt creating another booking, a new event can be created using the next version number, which would be 3 since the current version is 2:

{
    version: 3,
    type : "BookingCreated",
    seats : 6,
}

If uniqueness of the version is enforced by the database, then a successful commit to the database would indicate a successful booking; meaning the aggregate did not change in the database since it was last read.

However, if another event gets there first, it would have the same version (3), causing the commit to fail. That would be an indication that the number of seats changed under your feet.

That approach would avoid the data inconsistency. But you still need to handle the failure. For example, you can reload the aggregate, check if the event/command is still applicable and if so retry (up to a certain number of times).

0

Another solution could be that the entity which runs the events is a single threaded, so if you have multiple flights then you could have one such single thread for each flight.

Flight A - One thread handling events for it, a single event will update then both aggregates so one update would not overlap another update.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.