Skip to Content

Best Practices for Structuring Your Ruby on Rails Application


Ruby on Rails is celebrated for its convention-over-configuration approach, enabling developers to build robust applications quickly. However, as applications grow in complexity, maintaining clean, modular, and scalable code becomes a significant challenge. In this blog, we’ll explore the best practices to structure your Rails application effectively and ensure long-term maintainability.


1. Follow the MVC Pattern Closely

Rails is built on the Model-View-Controller (MVC) architecture, which stands for:

  • Model: Handles data, logic, and rules of the application.
  • View: Responsible for presenting data to the user.
  • Controller: Acts as a bridge between the model and the view, processing user input and updating the model or view accordingly.

This separation of concerns helps maintain a clean and organized codebase. For example:

# app/models/user.rb
class User < ApplicationRecord
  validates :name, presence: true
end

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def show
    @user = User.find(params[:id])
  end
end

# app/views/users/show.html.erb
<h1><%= @user.name %></h1>


2. Keep Controllers Thin

Fat controllers can lead to chaos in your codebase. A fat controller is one that contains too much business logic, making it hard to read and maintain. To avoid this:

  • Use Service Objects to extract business logic:
# app/services/payment_processor.rb
class PaymentProcessor
  def initialize(order)
    @order = order
  end

  def call
    # Payment processing logic
    @order.update(status: "paid")
  end
end
  • Move reusable logic to concerns or helper classes.
  • Use before_action filters for repetitive tasks like authentication or parameter sanitization:
class OrdersController < ApplicationController
  before_action :set_order, only: [:show, :edit, :update, :destroy]

  private

  def set_order
    @order = Order.find(params[:id])
  end
end


3. Avoid Fat Models

While models handle business logic, they shouldn’t become unmanageable. To prevent this:

  • Use ActiveRecord scopes for query logic:
class Order < ApplicationRecord
  scope :completed, -> { where(status: 'completed') }
  scope :recent, -> { order(created_at: :desc) }
end
  • Extract complex logic into concerns, service objects, or decorators:
# app/models/concerns/trackable.rb
module Trackable
  extend ActiveSupport::Concern

  included do
    before_create :set_tracking_code
  end

  private

  def set_tracking_code
    self.tracking_code = SecureRandom.hex(10)
  end
end


4. Use Concerns Wisely

Concerns allow you to share logic between models or controllers without duplication. However, don’t misuse them as a dumping ground for unrelated code.

Example of a Concern:

# app/models/concerns/archivable.rb
module Archivable
  extend ActiveSupport::Concern

  def archive
    update(archived: true)
  end
end


5. Organize Code with Service Objects and Presenters

Service objects handle business logic, while presenters or decorators enhance views with additional functionality. These patterns make your code modular and easier to test.

Example of a Presenter:

# app/presenters/order_presenter.rb
class OrderPresenter
  def initialize(order)
    @order = order
  end

  def formatted_total
    "#{@order.total_price} USD"
  end
end


6. Break Down the Views

Avoid embedding too much Ruby logic in your views. Use:

  • Partials to break down complex views:
<%= render 'shared/header' %>
  • Helpers for reusable methods:
module ApplicationHelper
  def format_price(price)
    number_to_currency(price, unit: "$")
  end
end
  • View Components for encapsulating UI logic:
# app/components/card_component.rb
class CardComponent < ViewComponent::Base
  def initialize(title:, content:)
    @title = title
    @content = content
  end
end
<%= render(CardComponent.new(title: "Welcome", content: "Hello, World!")) %>


7. Use the Right Tools for Testing

Testing is crucial for maintaining scalable applications. It ensures your code works as expected and helps prevent bugs when introducing new features or changes.


Why Testing Matters:

  • It builds confidence that your application behaves as intended.
  • It helps identify edge cases and potential errors early.
  • It simplifies debugging by pinpointing failing components.


Simple Example Using RSpec:

Let’s write a test for a User model:

  1. Add RSpec to your Gemfile:
    gem 'rspec-rails', group: [:development, :test]
    
    Run bundle install and initialize RSpec:
    rails generate rspec:install
    
  2. Create a test for the User model:
    # spec/models/user_spec.rb
    require 'rails_helper'
    
    RSpec.describe User, type: :model do
      it "is valid with a name" do
        user = User.new(name: "Alice")
        expect(user).to be_valid
      end
    
      it "is invalid without a name" do
        user = User.new(name: nil)
        expect(user).not_to be_valid
      end
    end
    
  3. Run the test:
    bundle exec rspec
    

This basic example demonstrates how to validate a User model using RSpec. As you grow more comfortable, you can write more complex tests for controllers, views, and integrations.

  • RSpec for a robust testing framework:
# spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  it "is valid with valid attributes" do
    expect(User.new(name: "John")).to be_valid
  end
end
  • FactoryBot for creating test data:
# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    name { "John" }
  end
end
  • Capybara for end-to-end testing.


8. Manage Dependencies Wisely

Avoid adding unnecessary gems to your project. Choose well-maintained and widely used gems. Regularly update dependencies and remove unused ones to keep your app lightweight.


9. Use Environment Variables

Sensitive data such as API keys and credentials should never be hardcoded. Instead, use environment variables to manage this data securely. For instance, you can utilize a gem like dotenv to load variables from a .env file or configure Rails credentials for enhanced security.


Example for Beginners:

  1. Install the dotenv gem by adding it to your Gemfile and running bundle install:
    gem 'dotenv-rails', groups: [:development, :test]
    
  2. Create a .env file in the root directory of your Rails application and add your environment variable:
    STRIPE_SECRET_KEY=sk_test_12345
    
  3. Update your application code to use the environment variable:
    # config/initializers/stripe.rb
    Stripe.api_key = ENV['STRIPE_SECRET_KEY']
    
  4. Make sure to add .env to your .gitignore file to avoid committing sensitive data:
    echo ".env" >> .gitignore
    

By following these steps, you can safely manage sensitive configuration data in your Rails application. Use environment variables and tools like dotenv or Rails credentials.


Example:

# config/initializers/stripe.rb
Stripe.api_key = ENV["STRIPE_SECRET_KEY"]

To set an environment variable locally, use:

echo "STRIPE_SECRET_KEY=sk_test_12345" >> .env


10. Document Your Code

Good documentation helps maintain clarity in your application. Use inline comments and tools like YARD to document classes and methods:

# app/models/user.rb
# Represents a user in the system
# @attr [String] name The name of the user
class User < ApplicationRecord
  validates :name, presence: true
end


11. Regularly Refactor

Refactoring is essential for maintaining clean code. Schedule time to refactor as your app grows, and look for areas to simplify or optimize.


Conclusion

By following these best practices, you can create a Ruby on Rails application that is clean, modular, and scalable. While every project is unique, adhering to these guidelines ensures your codebase remains maintainable and efficient, even as your application evolves.

How to Install and Set Up Next.js for Beginners