Intro

For someone who is experiencing Rails fatigue, it would be worth looking into alternatives for developing architectures with Ruby. It does not mean that it would require abandoning Rails at all because there are solutions that allow developing DDD concepts with the most popular Ruby framework. I will leave a discussion about whether you need this architecture approach in your app or not and only focus on how to start with DDD if you are not familiar with it.

Possible solutions

Based on my research, it looks like there are two possible ways to go with DDD in Ruby. The first one is Eventide with code examples available here: https://github.com/eventide-examples
These are pure Ruby examples. There is also the Eventide Rails project. However, not much is going there:
https://github.com/eventide-project/eventide-rails
The second one is the Rails Event Store with a demo app: https://github.com/RailsEventStore/cqrs-es-sample-with-res
It is integration with Rails, and a demo is also available on Heroku, which makes it very easy to play with it.

After having a look into Eventide examples and the CQRS project, what is similar between them is the concept of Commands and Events. For someone familiar with Rails who want to get hands dirty with Domain-Driven Design, I believe that the great place to start is cqrs-es-sample-with-res repo from Rails Event Store. So let’s have a look.

Rails Event Store

As you probably know from the previous section, the gem that brings all the necessary things to start the journey with Domain-Driven Design and Rails is called Rails Event Store.
The demo repo is actively maintained, consists of a use case of an e-commerce store with the implementation of around ten events that model business operations. For those who want to learn more, there are also Docs available here: https://railseventstore.org/

I decided to run a demo project locally, generate some events, and then connect database GUI client to see how Events have been stored in the database and what’s going on with other data like typical Rails models. Later on, to fully understand how things work, I added my custom Event.

Adding custom event

Step 1. Trigger command in Rails controller.

This part is easy. So here is where all the journey starts. I decided to add a tracking event when the user visits the Order page, so I need to call the following code in OrdersController#show action:

1
2
3
4
5
6
# app/controllers/orders_controller.rb
def show
@order = Orders::Order.find(params[:id])
@order_lines = Orders::OrderLine.where(order_uid: @order.uid)
+ command_bus.(Ordering::VisitOrder.new(order_uid: @order.uid))
end

The integration of this code with a controller requires changes in two other places. We need to register our command:

1
2
3
4
5
6
# ordering/lib/ordering/visit_order.rb
+module Ordering
+ class VisitOrder < Command
+ attribute :order_uid, Types::UUID
+ end
+end

To finalize this part, the configuration for our command bus is needed:

1
2
3
4
# config/initializers/rails_event_store.rb
Rails.configuration.command_bus.tap do |bus|
+ bus.register(Ordering::VisitOrder, Ordering::OnVisitOrder.new)
end

Step 2. Command Handler.

The next step is to handle our Command in the following file:

1
2
3
4
5
6
7
8
9
10
11
12
# ordering/lib/ordering/on_visit_order.rb
+module Ordering
+ class OnVisitOrder
+ include CommandHandler
+
+ def call(command)
+ with_aggregate(Order, command.order_uid) do |order|
+ order.visit
+ end
+ end
+ end
+end

The code above calls the Order#visit instance method, which implements the following code:

1
2
3
4
# ordering/lib/ordering/order.rb
+def visit
+ apply OrderVisited.new(data: {order_id: @id})
+end

Step 3. The last part, Event.

And here is the moment where I got stuck for a moment. I got the following error:

1
Missing handler method apply_order_visited on aggregate Ordering::Order

Let’s fix it by adding the following code in the same file:

1
2
3
4
# ordering/lib/ordering/order.rb
+on OrderVisited do |event|
+ # do nothing
+end

The last part is to handle our Event. To do this, we need to update our code in a few places. Add an Event:

1
2
3
4
5
6
# ordering/lib/ordering/order_visited.rb
+module Ordering
+ class OrderVisited < Event
+ attribute :order_id, Types::UUID
+ end
+end

Add subscription in config:

1
2
3
4
#config/initializers/rails_event_store.rb
Rails.configuration.event_store.tap do |store|
+ store.subscribe(Orders::OnOrderVisited, to: [Ordering::OrderVisited])
end

And the final part where the actual business logic happens is our read model. This read model is placed all together with other models like order or order_line (ActiveRecord):

1
2
3
4
5
6
7
8
9
10
11
# app/read_models/orders/on_order_visited.rb
+module Orders
+ class OnOrderVisited
+ def call(event)
+ order = Order.find_by(uid: event.data[:order_id])
+
+ order.visit_count += 1
+ order.save!
+ end
+ end
+end

Conclusion

Because there is no Demo app for the Eventide project that shows how to integrate it with Rails, I decided to go with RES to learn more about DDD. For someone already familiar with DDD concepts, it would be good to give Eventide repo a shoot. Check this blog post for more info: https://blog.arkency.com/my-first-10-minutes-with-eventide/