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:

  1. Active
  2. In Checkout
  3. Awaiting Payment Submission
  4. Awaiting Payment Confirmation
  5. Paid
  6. Cancelled

And the magic of events:

  1. Cart Active
  2. Item Added
  3. Item Removed
  4. Checking Out
  5. Payment Submitted
  6. Payment Received
  7. 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.