Sample CQRS / ES application gone wrong

In my post Building an Event Sourced application I’ve included sample code to setup denormalizers (event handlers) that will build a read model:

def event_store
  @event_store ||= RailsEventStore::Client.new.tap do |es|
    es.subscribe(Denormalizers::Router.new)
  end
end

One router to rule them all

Because that is only a sample application showing how easy is to build an Event Sourced application using Ruby/Rails and Rails Event Store there were some shortcuts. Shortcuts that should have never been there. Shortcuts that have made some doubts for others who try to build their own solution.

The router was defined as:

module Denormalizers
  class Router
    def handle_event(event)
      case event.event_type
      when Events::OrderCreated.name      then Denormalizers::Order.new.order_created(event)
      when Events::OrderExpired.name      then Denormalizers::Order.new.order_created(event)
      when Events::ItemAddedToBasket      then Denormalizers::OrderLine.new.item_added_to_basket(event)
      when Events::ItemRemovedFromBasket  then Denormalizers::OrderLine.new.item_removed_from_basket(event)
      end
    end
  end
end

And denormalisers were implemented as:

module Denormalizers
  class Order
    def order_created(event)
      # ...
    end

    def order_expired(event)
      # ...
    end
  end
end

But we could remove it completely and we do not need that case at all!

All this code could be rewritten using rails_event_store subscriptions as follows:

#command handler (or anywhere you want to initialise rails_event_store
def event_store
  @event_store ||= RailsEventStore::Client.new.tap do |es|
    es.subscribe(Denormalizers::OrderCreated.new, ['Events::OrderCreated'])
    es.subscribe(Denormalizers::OrderExpired.new, ['Events::OrderExpired'])
    es.subscribe(Denormalizers::ItemAddedToBasket.new, ['Events::ItemAddedToBasket'])
    es.subscribe(Denormalizers::ItemRemovedFromBasket.new, ['Events::ItemRemovedFromBasket'])
  end
end

#sample event handler (denormaliser)
module Denormalizers
  class OrderCreated
    def handle_event(event)
      # ... denormalisation code here
    end
  end
end

You see? No Router at all! It’s event store who “knows” where to send messages (events) based on subscriptions defined.

Implicit assumptions a.k.a conventions

Sometimes when you have a simple application like this it is tempting to define “convention” and avoid the tedious need to setup all subscriptions. It seems to be easy to implement and (at least at the beginning of the project) it seems to be elegant and simple solution that would do “the magic” for us.

# WARNING: not recommended code ahead ;)
def event_store
  @event_store ||= RailsEventStore::Client.new.tap do |es|
    get_all_events_defined.each |event_class|
      handlers_for(event_class).each |handler|
        es.subscribe(handler, [event_class.to_s])
      end
    end
  end
end

def get_all_events_defined
  [ Events::OrderCreate, Events::OrderExpired, Events::ItemAddedToBasket, Events::ItemRemovedFromBasket ]
  # or implement some more sophisticated way of getting all event's classes ;)
end

def handlers_for(event_class)
  handler_class = "Denormalizers::#{event_class.name.demodulize}".constantize
  handler_class.new
end

Naming is important! If we do not use convention but instead implicit assumption we will realise that it is not that simple and elegant at it looks like. Even worse, project tent to grow. When you will start using domain events you will want more and more of them. You could even want to have several handles for a single event ;) And maybe your handlers will need some dependencies? … Here is the moment when your simple convention breaks!

Make implicit explicit!

By coding the subscriptions one by one, maybe grouping them in some functional areas (bounded context) and clearly defining dependencies you could have more clear code, less “magic” and it should be easier to reason how things work.