The design pattern I swear by when developing large Ruby on Rails applications

I’ve worked with many large Ruby on Rails applications, some of which are upwards of 800 unique models. They’re not necessarily fun to work with when you have enough developers that you can even have an application with 800 models. Over the years I have found one pattern that I always go back to. Its roots are in domain-driven design and I think you might just like it enough to give it a try.

Contents

Our real-world example

Let’s pretend we have a massive application and in just one part of this application is a shopping cart feature. This post will discuss developing a shopping cart where a user can create a new cart and add items to this cart.

The lazymans schema

$ rails g model User name 
$ rails g model Product name 
$ rails g model Cart user:references
$ rails g model CartItem cart:references product:references

But we’re not the lazyman.

The better schema

$ rails g model User name
$ rails g model Product name
$ rails g model ShopingCart::Cart user:references
$ rails g model ShoppingCart::CartItem cart:references product:references

At least, better in the mind of this developer.

The routes

The routes would look something like this:

GET /products
GET /products/:id

GET /shopping_cart/cart # singular resource 
POST /shopping_cart/cart 
DELETE /shopping_cart/cart

POST /shopping_cart/cart/cart_items
PUT /shopping_cart/cart/cart_items/:id
DELETE /shopping_cart/cart/cart_items/:id

Some setup

Layouts

I have a master app/views/layouts/application.html.erb that is bare, bare, bare. Every other layout inherits from it, probably.

I drop this into my app/helpers/application_helper.rb:

def parent_layout(layout)
  @view_flow.set(:layout, output_buffer)
  output = render(file: "layouts/#{layout}")
  self.output_buffer = ActionView::OutputBuffer.new(output)
end

This allows me to, in my theoretical app/views/shopping_cart_layout.html.erb:

<% parent_layout :application %>

Routes

Because we’re working with a large application, I like clean routes.

Drop this in an initializer, or initialize it otherwise:

class ActionDispatch::Routing::Mapper
  def draw(routes_name)
    instance_eval(File.read(Rails.root.join("config/routes/#{routes_name}.rb")))
  end
end

Which allows us to, in our config/routes.rb:

Rails.application.routes.draw do 
  resources :products, only: [:index, :show]
  draw :shopping_cart 
end

Where config/routes/shopping_cart.rb looks like:

Rails.application.routes.draw do 
  namespace :shopping_cart do 
    resource :cart, only: [:create, :show, :destroy]
  end 
end 

Mailers

I hate seeing app/views/user_mailer/welcome.html.erb in my views folder.

In app/mailers/application_mailer.rb:

class ApplicationMailer < ActionMailer::Base
  append_view_path Rails.root.join('app', 'views', 'mailers')  
end

This allows us to shove all our mailer views into app/views/mailers/*. Much better.

Namespacing, namespacing, namespacing

The key to this is having an abstract-ish class for all of our ShoppingCart features: Cart, CartItem.

We want our directory tree to look like this:

├── app
│   ├── controllers
│   │   ├── concerns
│   │   │   ├── shopping_cart
│   │   │   │    └── find_cartable.rb
│   │   ├── shopping_cart
│   │   │   ├── carts
│   │   │   │   └── cart_controller.rb
│   │   │   │   └── cart_items_controller.rb
│   │   │   └── carts_controller.rb
│   │   │   └── shopping_cart_controller.rb
│   ├── finders
│   │   ├── shopping_cart
│   │   │   └── cart_finder.rb
│   │   │   └── cart_item_finder.rb
│   │   │   └── shopping_cart_finder.rb
│   ├── jobs
│   │   ├── shopping_cart
│   │   │   └── shopping_cart_job.rb
│   ├── mailers
│   │   ├── shopping_cart
│   │   │   └── cart_mailer.rb
│   │   │   └── shopping_cart_mailer.rb
│   ├── models
│   │   ├── shopping_cart
│   │   │   └── cart.rb
│   │   │   └── cart_item.rb
│   │   │   └── shopping_cart_record.rb
│   │   └── shopping_cart.rb
│   ├── serializers
│   │   ├── shopping_cart
│   │   │   └── shopping_cart_serializer.rb
│   │   │   └── cart_serializer.rb
│   │   │   └── cart_item_serializer.rb

When you’re working with larger applications, you might find yourself creating DSLs and APIs specific to each feature. Having abstract classes for your feature can carry these DSLs and APIs until they’re needed for other parts of your large application.

This pattern is applied to everything, from serializers to decorators to jobs/workers. There will be a shopping_cart_serializer.rb, a shopping_cart_decorator.rb, and shopping_cart_job.rb. Concerns are placed in a shopping_cart folder.

Controllers

I nest my controllers like I do my routes. This reduces cognitive overhead.

Of note, shopping_cart/shopping_cart_controller.rb will act as an ApplicationController for all of our shopping cart logic: everything will inherit from it. Similarly, shopping_cart/carts/cart_controller.rb will do the same for all of its subresources. Granted, in this example, we only have one (cart_items_controller.rb) but you can imagine that this can quickly grow for a non-trivial example in a blog post from a guy that only pretends to know what he’s talking about.

This pattern is applied to everything, from serializers to decorators to jobs/workers. There will be a shopping_cart_serializer.rb, a shopping_cart_decorator.rb, and shopping_cart_job.rb. Concerns are placed in a shopping_cart folder.

When you’re working with larger applications, you might find yourself creating DSLs and APIs specific to each feature. Having these abstract classes can carry these DSLs and APIs until they’re needed for other parts of your large application.

Here’s what your controllers look like:

In app/controllers/shopping_cart/shopping_cart_controller.rb:

module ShoppingCart
  class ShoppingCartController < ApplicationController
    # shared logic goes in here
  end 
end 

In app/controllers/shopping_cart/carts/cart_controller.rb:

module ShoppingCart
  module Carts
    class CartController < ShoppingCartController
      private 

      def current_cart
        @current_cart ||= current_user.carts.find(params[:id])
      end  
    end 
  end 
end 

In app/controllers/shopping_cart/carts/cart_items_controller.rb:

module ShoppingCart
  module Carts
    class CartItemsController < CartController
      def create
        @cart_item = current_cart.cart_items.create(cart_item_params)
        # ... 
      end 
    end 
  end 
end 

Models

Same applies with our models:

In app/models/shopping_cart/shopping_cart_record.rb:

module ShoppingCart
  class ShoppingCartRecord < ApplicationRecord 
    self.abstract_class = true 
  end 
end

In app/models/shopping_cart/cart.rb:

module ShoppingCart
  class Cart < ShoppingCartRecord 
    # ... 
  end 
end

Conclusion

Sticking with conventions and have clear separations of concerns makes developing large Ruby on Rails applications actually fun. This may mean a little planning and a little forethought, but that’s what makes a good developer.

Changelog

This is a living document.