RL ROLAND LOPEZ
// 2 min read

Ruby on Rails and the Art of Object-Oriented Design

Ruby on Rails and the Art of Object-Oriented Design β€” Rails' conventions guide you toward design patterns, but real maintainability comes from applying SRP, delegation, and SQL-first thinking from day one.

Emphasizing maintainability and scalability through design

Ruby on Rails, by virtue of its opinionated nature, guides developers towards certain design patterns and practices.

However, even within these constraints, there is significant room for design decisions that impact the future maintainability and scalability of applications.

Let’s explore how to leverage object-oriented design principles to craft better Rails applications.

PS: Most perf issues come from bad designs (i.e. bad choices).

Understanding the cost of poor design

Consider the long-term implications of the initial code arrangement.

Rails applications that start with a tight, interdependent object mesh often face rising costs in maintenance as they scale.

This problem intensifies when the codebase expands to accommodate new features.

The principle here is straightforward:

The cost of modifying an application increases disproportionately to its complexity.

Example of poorly designed Rails code

# app/models/user.rb
class User < ApplicationRecord
  has_many :projects

  def total_project_hours
    projects.sum { |p| p.total_hours }
  end
end
# app/models/project.rb
class Project < ApplicationRecord
  belongs_to :user
  has_many :tasks

  def total_hours
    tasks.sum(:hours)
  end
end
class Task < ApplicationRecord
  belongs_to :project
end

In the User model, notice the method total_project_hours, which traverses through projects to calculate hours.

This not only violates the Single Responsibility Principle by placing additional responsibility on the User model, but it also creates a potential performance bottleneck as the number of projects grows:

  • total_project_hours in User does in Ruby what can be done in SQL.
# app/models/user.rb
class User < ApplicationRecord
  has_many :projects
  has_many :tasks, through: :projects

  delegate :sum_hours, to: :tasks, prefix: :tasks
end
# app/models/project.rb
class Project < ApplicationRecord
  belongs_to :user
  has_many :tasks
end
class Task < ApplicationRecord
  belongs_to :project

  scope :sum_hours, -> { sum(:hours) }
end

Now total_project_hours does not exist anymore, and you access the same data using:

  • @user.tasks_sum_hours

This gives you the sum of all the tasks in all the projects for one user.

Moving the hours calculation out of User aligns with the Single Responsibility Principle, simplifying the model and delegating the responsibility for calculating hours to where the data lives.

Conclusion

The essence of good object-oriented design in Rails is not merely about adhering to its conventions but understanding and applying design principles that safeguard future scalability and maintainability.

By structuring code with an eye on future change, you reduce the costs and complexities involved in maintaining and evolving your Rails application.

Roland Lopez
Written by
Roland Lopez

Technical co-founder specialized in SaaS, DevOps, AI agents, and data platforms. Building and scaling with Ruby on Rails, n8n, and fast feedback loops.

Built by Agent Skynet See the agency