5 minutes
My Favourite Design Pattern
As I might have mentioned before, tracking the state of an entity is an integral part of writing correct and bug-free code. Through some trial and error I have settled on a specific way of handling this in larger, complex systems. I was pleasantly surprised when I found this pattern already exists and described. Finally, I had a name to go with this approach! - Event Sourcing.
What is Event Sourcing?
Essentially this builds on the Domain Driven Design (DDD) pattern called Command Query Responsibility Segregation or CQRS. I like the straight forward and simple explanation given in the rust documentation. Source
Event sourcing adds to the flexibility of CQRS by relying upon the events as our source of truth
I love DDD and its approach to simplifying complex solutions and this specific pattern I’ve found to be exceptionally useful.
Example
There’s a pretty good example in the rust documentation there, but I’m going to try and illustrate my approach in practice. In my opinion, DDD is not a solid rule set and is always open to interpretation.
I’m going to use the example of a shopping cart with the following possible states:
- Active
- In Checkout
- Awaiting Payment Submission
- Awaiting Payment Confirmation
- Paid
- Cancelled
And the magic of events:
- Cart Active
- Item Added
- Item Removed
- Checking Out
- Payment Submitted
- Payment Received
- Cart Cleared
Each of these states need to account for at least the following:
- Validation
- The cart cannot move to the Awaiting Payment state if it is not currently in Active
- Error handling
- If validation fails, it needs to be handled gracefully, with a good error message explaining why the operation failed in detail
- Encapsulation
- The logic should only be concerned with what it does in its current state
- Historical logging
- We need to know when a state changed and why
- Flexibility
- We may need to add, remove or modify states in the future
- Implementation of logic on related entities
- Importantly, changing of a state usually requires actions on other areas of the system
To show how all this is managed in a simple and straightforward way, here’s some (bad) pseudo code:
CartEventHandler class
// These are our public entry points
public createCart(CartData)
// Validate that the cart doesn't exist already and make sure the input is correct
// Throw a detailed custom ValidationException with an error message and the Cart data if it fails
// Important: Wrap the entire call stack in a database transaction. If anything fails, it will fail completely without fallout to other entities
try transaction
cart = CartFactory::Cart(CardData)
cart = mainHandler(cart, CartEvent::CartActive)
catch (CartException)
rollback transaction
public addItemToCart(Cart, CartItem)
try transaction
Cart::addItem(CartItem}
cart = mainHandler(cart, CartEvent::ItemAdded)
catch (CartException)
rollback transaction
public checkoutCart(Cart)
try transaction
cart = mainHandler(cart, CartEvent::CheckingOut)
catch (CartException)
rollback transaction
public awaitingPayment(Cart, Invoice)
try transaction
cart = mainHandler(cart, CartEvent::PaymentSubmitted)
InvoiceEventHandler::doSomething(Invoice)
catch (CartException)
rollback transaction
...
private logCartEvent(CartEvent)
private setCartStatus(CartStatus)
// We can check that we don't write duplicate status here
private mainHandler(Cart, CartEvent)
cartStatus = Cart::getStatus()
switch CartEvent
case CartActive
cartStatus = Active
break
case ItemAdded
// Check state logic: Can only move here if current state = Active
Cart::addItem(Item)
break
case ItemRemoved
// Check state logic: Can only move here if current state = Active
Cart::removeItem(Item)
break
case CheckingOut
// Check state logic: Can only move here if current state = Active
cartStatus = InCheckout
break
case PaymentSubmitted
// Check state logic: Can only move here if current state = CheckingOut
cartStatus = AwaitingPaymentConfirmation
break
case PaymentReceived
// Check state logic: Can only move here if current state = PaymentSubmitted
cartStatus = Paid
break
...
logCartEvent(CartEvent)
setCartStatus(cartStatus)
return Cart
Now let’s dive deeper into each section.
public addItemToCart(Cart, CartItem)
try transaction
Cart::addItem(CartItem}
cart = mainHandler(cart, CartEvent::ItemAdded)
catch (CartException)
rollback transaction
...
First off, we have our publicly accessible functions. These are our entry points for logic, they define inputs required, do validation on the data and act on other entities if needed. If an Exception is thrown, they will roll back the entire database transaction so that no data is persisted.
private logCartEvent(CartEvent)
private setCartStatus(CartStatus)
// We can check that we don't write duplicate status here
Next we have some helper functions, I’ve decided to have these because I don’t want the code to update the database record with a state that the record is already in. This is a valid possibility here.
private mainHandler(Cart, CartEvent)
cartStatus = Cart::getStatus()
switch CartEvent
case CartActive
cartStatus = Active
break
case ItemAdded
case ItemRemoved
// Check state logic: Can only move here if current state = Active
// No state to update here, so we just fall through and log the event
break
case CheckingOut
// Check state logic: Can only move here if current state = Active
cartStatus = InCheckout
break
case PaymentSubmitted
// Check state logic: Can only move here if current state = CheckingOut
cartStatus = AwaitingPaymentConfirmation
break
case PaymentReceived
// Check state logic: Can only move here if current state = PaymentSubmitted
cartStatus = Paid
break
...
logCartEvent(CartEvent)
setCartStatus(cartStatus)
return Cart
Finally, we come to the main handler where the event and state logic combine and the magic happens. This function is private as access should go through the public entry points. It does only the following: It validates that the event’s state logic is correct and persists it or “Do one thing and do it well”. This means it is trivial to add a new event or state. I find it also reads very easily and documents the core logic very well.
Conclusion
I hope this illustrates the beauty of this pattern. I just love how simple it actually is and how it makes a complex state management system easy to grok and extend. It works even better when combined with other DDD concepts, such as ValueObjects, Repositories and Services.