Make your Ruby code more modular and functional with polymorphic aggregate classes

For the last year, I’ve been using Rails (as I do for the last 10 years), but last year it was almost exclusively Rails “Not the Rails Way”. We have successfully combined Rails with Domain-Driven Design, CQRS and Event Sourcing. Over the last year, most of the business logic state in my apps was persisted using events, not the “let’s just store the last state and forget the history” ;)

In short, I was using a lot of Event Sourcing.

Event Sourcing is not easy and it does take time to get used to it. We are event sourcing our aggregates. Aggregates are like the more important objects in our systems, like Order, Product, User, Project etc.

Aggregates are just normal objects, but if you combine them with event sourcing they take this specific shape. We have developed a library called AggregateRoot (part of our RailsEventStore ecosystem of tooling), which helps a lot with Event Sourcing.

Once you start modelling your domain (aka what objects we should have and how they communicate), you will arrive to the problem of finding ways of making your aggregates smaller.

Today, I’d like to share an experimental technique with you. For now it’s just a proof of concept, but hopefully soon this will be something I will be able to use in my apps.

Usually, I’d have a class like Order and it would have several methods, like:


class Order
  def place
  end

  def pay
  end

  def cancel
  end
end

Depending on the current state of the order, we’d disallow certain actions, either via exception or whichever else favourite technique for returning the result:

class Order
  def place
  end

  def pay
    raise OrderNotPlacedYet if @state != :placed
  end
end

In a way, this is like a state machine and could be implemented with some help of the state_machine gems or similar.

I wanted to try out a different attempt. For each state of the order let’s have a separate class and use polymorphism to call the methods.

The initial proof of concept looks like this:

class OrderPlaced    < RailsEventStore::Event; end
class OrderPaid      < RailsEventStore::Event; end
class OrderCancelled < RailsEventStore::Event; end

class Order
  include AggregateRoot

  def place
    apply(OrderPlaced.new)
  end

  private

  def apply_order_placed(_)
    PlacedOrder.new
  end
end

class PlacedOrder
  include AggregateRoot

  def pay
    apply(OrderPaid.new)
  end

  private

  def apply_order_paid(_)
    PaidOrder.new
  end
end

class PaidOrder
  include AggregateRoot

  def cancel
    apply(OrderCancelled.new)
  end

  private

  def apply_order_cancelled(_)
    CancelledOrder.new
  end
end

class CancelledOrder
  include AggregateRoot
end

As you can see after every call to the public method, we call the apply method - this is provided by the AggregateRoot gem. Under the hood it makes sure the event is published and then it calls the appropriate private method. Those private methods usually just set the state. However, in my spike, they actually also return a new kind of object, depending on the state.

It’s still not production-ready, but I find it a good start for more research. I like how the classes become small.

Most importantly I like how all the if statements disappear and I don’t even need to signal the result anymore. In pure OOP, calling other objects is sending messages to them. In Ruby it’s actually exactly like that. Under the hood, we have the send mechanism for that. If no such method exists, then the message can’t be delivered. In a way, this is relying on the message-driven approach in Ruby objects.

Another nice side-effect (pun intended) is the fact that the objects are now immutable. They don’t change existing state, they return a new state instead. Which brings OOP and FP nicely together in a nice domain modelling example.

If you want to learn more about aggregates and event sourcing with Ruby/Rails consider buying our “Domain-Driven Rails” book.

If you wonder where such a OOP/FP merge can bring us, consider learning more about Serverless, which is like a DDD/Microservices/FP heaven.

Thanks for reading!