Custom Event Sourcing

I seem to be late to the Event Sourcing architecture party, like several years late. I initially looked into it sometime ago, but didn’t have any immediate application use-case for it, so filed it away until recently when building an enterprise Rails application that tracks daily production activities, and also income and expense (Accounts, yo!).  

Enter Event Sourcing…

In its basic form, Event Sourcing revolves around aggregates – a body of interconnected entities (models) which together complete a task. The classic example is that of an Order: select order items, make payment, order shipped, order delivered. These activities (or events) leave a trail of interesting data points both at the merchant and buyer ends. The aggregate here is composed of :order, :payment, :shipment and :item entities, with :order serving as aggregate_root

Implementing Event Sourcing in Rails

There are a few ideas to implementing Event Source I felt were worthy of trying out. These include setting up rails_event_store gem and CommandBus with Redis, and using ActiveRecord::Observer to track model changes. But after reading a bunch of articles on the subject, I thought to bite the bullet, and roll my own implementation, while drawing inspiration from Kickstarter’s guide – their barebones no frills approach offers me customization opportunities down the line.

What We Will be Building

Our example will be based on an Account Reconciliation app that keeps track of a small business’ income and expenses. These (Account, Income, and Expenses) form an Aggregate cluster with Account as Aggregate Root, and Income & Expense serving as events. When a business receives or pays out money, we would expect it to impact the Accounts. 

The Solution Design

With my (admittedly limited) understanding of Event Sourcing, I decided to model the solution as a Multi-Table Inheritance, and got some help from here. This gave me some quick wins, and I was able to get my model running with this setup:

    class Account < ApplicationRecord    
       # attributes :amount, :balance, :data, :status    
       enum status: { pending_approval: 0, paid: 1, deposited: 2, failed: 3 }    

       before_validation :initialize_model    

       validates :amount, presence: true,  numericality: { greater_than_or_equal_to: 0 }

       # to be implemented in subclasses that are part of the Account aggregate
       def initialize_model
           raise NotImplementedError, 'Subclass did not define #initialize_model'
       end

    end
    class Account::Salary < Account    
        has_one :event, class_name: 'SalaryEvent', inverse_of: :salary, dependent: :destroy, autosave: true
    
        delegate :basic, :basic=, :allowances, :allowances=, :tax_payable, :tax_payable=, :prepared_by, :prepared_by=, :approved_by, :approved_by=, to: :lazily_built_event
    
        validates :basic, :allowances, :tax_payable, presence: true, numericality: { only_integer: true, greater_than: 0 }
    
        def lazily_built_event        
            event || build_event   
        end

        # implement the inherited method. This doubles as our Calculator    
        def initialize_model        
            amount = total_cost        
            data = { basic: basic, allowances: allowances, tax_payable: tax_payable, prepared_by: prepared_by, approved_by: approved_by }.as_json            
            
            if approved_by            
                balance = get_balance - total_cost
                status = 'paid'         
            else            
                status = 'pending_approval'  
            end    
        end

        private

        def total_cost
            basic + allowances + tax_payable
        end

        def get_balance
            (balance || 0)
        end

    end
    class SalaryEvent < ApplicationRecord    
        # attributes :basic, :allowances, :tax_payable, :prepared_by, approved_by  
        belongs_to :salary, class_name: 'Account::Salary', inverse_of: :event

        validates :salary, presence: true
    end

With this implementation, I was able to get AggregateRoot, Events, and Calculator functional. Next up: Reactors & Dispatch

Conclusion

This is a simplistic Account tracking application. The goal is to show some nuances with Event Sourcing that warrants pecuniar treatment (e,g. Double Entry and Dual Control principles in Accounting)

See you next time!

Written on December 3, 2020