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_hoursinUserdoes in Ruby what can be done in SQL.
Recommended refactoring
# 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.
Technical co-founder specialized in SaaS, DevOps, AI agents, and data platforms. Building and scaling with Ruby on Rails, n8n, and fast feedback loops.
Design Patterns in Rails
When and how to use Singleton, Factory, and other design patterns in Rails. Concrete examples, anti-patterns to avoid, and a rule of thumb for overuse.
Redesign Your Models for Performance in Rails
Most performance issues come from over-normalized models. Learn how scopes, denormalization, and JSON columns simplify queries and remove painful joins.
Refactoring Fat Models in Rails: Delegate, Extract, and Compose
Reduce complexity in Rails models with method delegation, extraction into service classes, and composed_of - keeping ActiveRecord lean and SRP-aligned.