A dead simple example of event-sourcing by using event-modelling with Java 21.
- Booking seats among multiple rows.
- Booking seats in a single row by two concurrent commands that singularly do not violate any invariant rule and yet the final state is potentially invalid.
I have two rows of seats related to two different streams of events. Each row has 5 seats. I want to book a seat in row 1 and a seat in row 2. I want to do this in a single transaction so that if just one of the claimed seats is already booked then the entire multiple-row transaction fails and no seats are booked at all.
- Can you handle this in a transactional way?
- Can it handle more rows?
One invariant rule says that no booking can end up in leaving the only middle seat free in a row. The invariant rule must be preserved even if two concurrent transactions try to book the two left seats and the two right seats independently so violating (together) this invariant.
- How to lock any row
- Give timely feedback to the user
Install OpenJDK 21
with SDKMan
or any other tool you prefer.
Then if you're a Linux user like me, install make
then:
make run
otherwise:
java --source 21 --enable-preview SeatBooking.java
- command handled:
BookSeat(row = Row1, seat = Seat2, user = Merlin)
- command handled:
BookSeat(row = Row1, seat = Seat1, user = Wart)
- command handled:
BookSeat(row = Row1, seat = Seat2, user = Merlin)
- async. command handled:
BookSeat(row = Row2, seat = Seat2, user = Wart)
-> version may be inconsistent because of (5) - async. command handled:
BookSeat(row = Row2, seat = Seat2, user = Merlin)
-> version may be inconsistent because of (4) - command handled:
BookSeat(row = Row1, seat = Seat1, user = Wart)
1 -> Event Committed[id=de54786e-ad50-424c-8001-44cd36f9eb06, event=SeatBooked[row=Row1, seat=Seat2, user=Merlin], version=0, storedAt=2024-01-11T20:44:31.028653718] has been committed and emitted
2 -> Event Committed[id=f2994b63-2306-4c94-9cd1-16cba223d1d7, event=SeatBooked[row=Row1, seat=Seat1, user=Wart], version=1, storedAt=2024-01-11T20:44:31.039795412] has been committed and emitted
5 -> Event Committed[id=ea6c431a-4704-45eb-9bb2-eb63cdfcb9a2, event=SeatBooked[row=Row2, seat=Seat2, user=Merlin], version=2, storedAt=2024-01-11T20:44:31.045232365] has been committed and emitted
3 -> Can't book seat, command BookSeat[row=Row1, seat=Seat2, user=Merlin] with already booked seats
4 -> Can't commit event, event Uncommitted[event=SeatBooked[row=Row2, seat=Seat2, user=Wart], version=2] not consistent with version in event-store: 3
6 -> Can't book seat, command BookSeat[row=Row2, seat=Seat4, user=Wart] must book the middle seat
In case 1.
there's no issue Merlin can book seat 2 on row 1; the event-store version is 0.
In case 2.
there's no issue Wart can book seat 1 on row 1; the event-store version is 1.
As you can see from the output, instead of printing the error message thrown by case 3.
, the result for case 5.
is printed (because of virtual threads).
In case 5.
Merlin could book seat 2 on row 2; the event-store version is 2.
In case 4.
and case 5.
there's a concurrency issue Merlin (case 5.
) could book seat 2 on row 2, although Wart (case 4.
) tried to book the same seat before Merlin.
In case 3.
Merlin can't book the seat, since Wart already booked the same seat.
In case 6.
Wart can't book seat 4 on row 2, since a sided seat from the middle is already booked, therefore must book the middle seat.
case 4.
and case 5.
are exchangeable due to the asynchronous nature of virtual threads, therefore try to run the example multiple times to see it happen.
The optimistic lock is implemented as a show-of-concept and most of the time works as-is, but you might face the casual and rare case where case 4.
and case 5.
are both valid.