DEV Community

Cover image for Elemental Ruby: A Better Way to Organize Rails Applications
Bevin Hernandez
Bevin Hernandez

Posted on

Elemental Ruby: A Better Way to Organize Rails Applications

Every Rails developer has had that moment. You open a controller, and there it is: hundreds of lines of tangled business logic staring back at you. In my case, it was a reports controller that had grown to over 300 lines, mixing date parsing, filtering, business logic, and export handling into an incomprehensible mess. It didn't have tests, and the former developer had left the company — and I, the new hire, was being asked to change the functionality.

Let me show you something that resembles it without actually showing the old code:

def index
  # 300 lines of mixed concerns:
  @date = params[:date] ? Date.parse(params[:date]) : Date.current
  @report_type = params[:type] || default_report_type

  # Complex date logic
  if @view_type == "Day"
    # date calculations
  elsif @view_type == "Week"
    # more date calculations
  # ... several more conditions
  end

  # Business logic mixed with filtering
  @records = current_account.records
  @records = @records.where(complex_conditions)

  # Complex business rules
  if @report_type == "Summary"
    # 50 lines of summary logic
  elsif @report_type == "Detail"
    # 50 more lines of detail logic
  end

  # Export logic mixed in
  respond_to do |format|
    format.html
    format.csv { # complex CSV generation }
  end
end
Enter fullscreen mode Exit fullscreen mode

To say I was afraid to change it was an understatement. To boot, the method was deeply nested, and writing tests for it as it was with it's endlessly branching logic would be a feat that I wasn't up to. I was in need of another pattern.

The Traditional Approaches

So, I tried the standard Rails patterns:

Fat Models

class Report < ApplicationRecord
  def self.generate_summary
    # Move logic to model
    # Now model is huge instead of controller - not a solution
  end
end
Enter fullscreen mode Exit fullscreen mode

Concerns

module ReportGeneration
  extend ActiveSupport::Concern

  # Move logic to concern
  # Now complexity is just hidden - not a solution
end
Enter fullscreen mode Exit fullscreen mode

Service Objects

class GenerateReportService
  def call
    # Move logic to service
    # End up with explosion of services, 
    # and functional programming methods in an OO language/app 
    # just never felt right
  end
end
Enter fullscreen mode Exit fullscreen mode

Each of these approaches helped a bit, but they didn't quite solve the fundamental problem: the code wasn't organized around the actual business concepts it represented.

Enter Elemental Ruby

Elemental Ruby is a pattern that emerged from the intersection of several powerful ideas.

I wanted a lot in a solution - it had to be extensible, follow good design principles, be easily testable, maintainable, and to the extent possible, follow Rails conventions.

So I drew from:

  • Brad Frost's Atomic Design (breaking complex systems into fundamental units)
  • Sandi Metz's teaching about small, focused objects
  • Domain-Driven Design's bounded contexts
  • Rails' convention over configuration
  • SOLID design principles

The core idea is simple: organize your code around the fundamental elements of your business domain, using Ruby's natural namespacing capabilities.

Let's see how our reports controller evolves:

First Evolution: Service Objects

def index
  @date = DateParser.new(params[:date]).parse
  @report_type = ReportTypeSelector.new(current_account, params[:type]).select
  @date_range = DateRangeCalculator.new(@date, params[:view_type]).calculate
  @records = RecordFilter.new(current_account.records, params).filter

  @data = ReportGenerator.new(
    type: @report_type,
    records: @records,
    date_range: @date_range
  ).generate

  respond_with_report(@data)
end
Enter fullscreen mode Exit fullscreen mode

Better, but our business concepts are still scattered across multiple service objects. We've traded one form of complexity for another.

Final Evolution: Elemental Ruby

def index
  @date_range = Reports::DateRange.new(
    date: params[:date],
    view_type: params[:view_type]
  )

  @report = Reports::Type.new(
    account: current_account,
    type: params[:type]
  ).build

  @data = @report.generate(
    records: Reports::Records.new(current_account, params),
    date_range: @date_range
  )

  respond_with_report(@data)
end
Enter fullscreen mode Exit fullscreen mode

The business concepts are now clear and organized:

# app/models/reports/date_range.rb
class Reports::DateRange
  def initialize(date:, view_type:)
    @date = parse_date(date)
    @view_type = view_type
  end

  def start_date
    case @view_type
    when "Day"
      @date
    when "Week"
      @date.beginning_of_week
    # etc
    end
  end

  def header
    case @view_type
    when "Day"
      I18n.l(@date, format: :long)
    when "Week"
      "#{start_date.strftime('%B %e')} - #{end_date.strftime('%B %e')}"
    # etc
    end
  end

  private

  def parse_date(date)
    Date.parse(date)
  rescue
    Date.current
  end
end
Enter fullscreen mode Exit fullscreen mode

Why This Works Better

  1. Clear Organization

    • Each element represents a clear business concept
    • Related functionality stays together
    • Easy to find code by thinking about the domain
  2. Better Testing

    • Elements have clear responsibilities
    • Dependencies are explicit
    • Tests follow business concepts
  3. Easier to Change

    • Changes tend to affect single elements
    • New features have clear homes
    • Less risk of unintended consequences
  4. Better for Teams

    • New developers can understand the domain
    • Clear boundaries between different parts
    • Natural organization for dividing work

How This Aligns with SOLID

  1. Single Responsibility Principle

    • Each element handles one aspect of the domain
    • Reports::DateRange only handles date-related concepts
  2. Open/Closed Principle

    • New behavior can be added by creating new elements
    • Existing elements remain unchanged
  3. Liskov Substitution Principle

    • Elements use composition over inheritance
    • Avoid inheritance hierarchies entirely
  4. Interface Segregation

    • Elements expose only related methods
    • Clients depend only on what they need
  5. Dependency Inversion

    • Elements depend on abstractions
    • Implementation details stay private

When to Use Elements

Good candidates for elements:

  • Complex models (User, Account, Project)
  • Code that clusters around business concepts
  • Functionality that grows together

Don't create elements for:

  • Simple models (Tag, Category)
  • Single methods
  • Technical concerns

Getting Started

  1. Look for clusters of related methods in your models
  2. Identify business concepts in your controllers
  3. Create namespaced classes for each concept
  4. Move logic gradually
  5. Let your tests guide you

Conclusion

Elemental Ruby isn't a silver bullet, but it provides a clear path for organizing Rails applications around business concepts. It builds on Rails conventions while adding structure that helps applications grow sustainably.

The next time you open a controller and feel that familiar dread, remember: there's a better way to organize your code. Break it down into its elements, and let your business domain guide you.


This is the first in a series exploring Elemental Ruby. Next time we'll look at identifying elements in legacy code.

What patterns have you found for organizing Rails applications as they grow? Have you tried similar approaches? Let me know in the comments!

Top comments (0)