Maintainability is a design choice
Rails is opinionated, so it nudges you toward certain patterns. But there is still plenty of room for the decisions that decide how maintainable your app is later.
PS: most performance problems trace back to bad design choices.
The cost of poor design
Apps that start as a tight, interdependent mesh of objects get more expensive to maintain as they grow. Every new feature drags in more of the codebase.
The principle:
The cost of changing an app rises faster than its complexity.
A poorly designed example
# 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
Look at total_project_hours on User. It walks every project to add up hours.
That breaks the Single Responsibility Principle, since User should not own this, and it does in Ruby what SQL should do, so it slows down as projects grow.
The refactor
# 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 is gone. You get the same number with:
@user.tasks_sum_hours
That is the total hours across all tasks in all of a user’s projects.
The calculation lives where the data lives, User stays focused, and SQL does the summing.
Takeaway
Good object-oriented design in Rails is not just following conventions. It is applying the principles that protect you from future change: keep responsibilities where the data is, and structure code so tomorrow’s feature touches fewer files.