Rails Architecture Best Practices and Building Scalable Applications — Ruby Deep Dive[11]

Bhavyansh @ DiversePixel
4 min readJul 28, 2024

--

Let’s dive deeper in architecture best practices and building scalable applications in rails.

Moving Beyond MVC: When and How to Refactor

Identifying the Need for Refactoring

  • Fat Models/Controllers: When your models or controllers become too large and difficult to maintain.
  • Code Duplication: Repetitive code across the application.
  • Complex Business Logic: Business logic that doesn’t naturally fit into models or controllers.

Refactoring Strategies

  • Service Objects
  • Query Objects
  • Form Objects
  • Presenters/Decorators
  • Value Objects
  • Concerns

Service Objects: Encapsulating Complex Business Logic

Service Objects encapsulate complex business logic outside of models and controllers.

Example

# app/services/create_user_service.rb
class CreateUserService
def initialize(user_params)
@user_params = user_params
end
def call
User.create(@user_params)
end
end
# Usage in controller
class UsersController < ApplicationController
def create
@user = CreateUserService.new(user_params).call
if @user.persisted?
redirect_to @user, notice: 'User created successfully.'
else
render :new
end
end
private
def user_params
params.require(:user).permit(:name, :email)
end
end

Query Objects: Keeping Database Queries Clean and Reusable

Query Objects encapsulate database queries, making them reusable and easier to test.

Example

# app/queries/recent_users_query.rb
class RecentUsersQuery
def initialize(relation = User.all)
@relation = relation
end
def call
@relation.where('created_at >= ?', 1.week.ago)
end
end
# Usage
recent_users = RecentUsersQuery.new.call

Form Objects: Handling Complex Forms and Validations

Form Objects help manage complex forms with validations and processing logic.

Example

# app/forms/user_registration_form.rb
class UserRegistrationForm
include ActiveModel::Model
attr_accessor :name, :email, :password
validates :name, :email, :password, presence: true
def save
return false unless valid?
user = User.create(name: name, email: email, password: password)
user.persisted?
end
end
# Usage in controller
class RegistrationsController < ApplicationController
def create
@form = UserRegistrationForm.new(user_params)
if @form.save
redirect_to root_path, notice: 'Registered successfully.'
else
render :new
end
end
private
def user_params
params.require(:user_registration_form).permit(:name, :email, :password)
end
end

Presenter/Decorator Pattern: Keeping Views Clean

Presenters or Decorators add presentation logic to models, keeping views clean.

Example with Draper Gem

# Gemfile
gem 'draper'
# app/decorators/user_decorator.rb
class UserDecorator < Draper::Decorator
delegate_all
def full_name
"#{object.first_name} #{object.last_name}"
end
end
# Usage in views
<%= @user.decorate.full_name %>

Value Objects: Encapsulating Domain Concepts

Value Objects represent domain concepts that have no identity beyond their attributes.

Example

# app/models/money.rb
class Money
include Comparable
attr_reader :amount, :currency
def initialize(amount, currency)
@amount = amount
@currency = currency
end
def <=>(other)
amount <=> other.amount
end
def to_s
"#{amount} #{currency}"
end
end
# Usage
price = Money.new(100, 'USD')
puts price.to_s # "100 USD"

Concerns: When to Use (and When to Avoid)

Concerns modularize shared functionality, but overuse can lead to spaghetti code.

Example

# app/models/concerns/timestampable.rb
module Timestampable
extend ActiveSupport::Concern
included do
before_create :set_created_at
before_update :set_updated_at
end
private
def set_created_at
self.created_at ||= Time.current
end
def set_updated_at
self.updated_at = Time.current
end
end
# app/models/user.rb
class User < ApplicationRecord
include Timestampable
end

Event-Driven Architecture in Rails Applications

Event-driven architecture decouples components and makes the system more scalable.

Example with rails_event_store Gem

# Gemfile
gem 'rails_event_store'
# app/events/user_created_event.rb
class UserCreatedEvent < RailsEventStore::Event
def self.create(user)
new(data: { user_id: user.id, name: user.name })
end
end
# Triggering an event
Rails.configuration.event_store.publish(UserCreatedEvent.create(user))
# Handling an event
class NotifyAdminOnUserCreated
def call(event)
AdminMailer.new_user(event.data[:user_id]).deliver_later
end
end
Rails.configuration.event_store.subscribe(
NotifyAdminOnUserCreated.new,
to: [UserCreatedEvent]
)

Building Scalable and Maintainable Rails Applications

Designing for Modularity with Rails Engines

Rails Engines encapsulate functionality into isolated modules, enhancing modularity.

Example

# Generate a new engine
rails plugin new blorgh --mountable
# Mounting the engine
# config/routes.rb
mount Blorgh::Engine, at: "/blorgh"
# Engine's routes
# blorgh/config/routes.rb
Blorgh::Engine.routes.draw do
resources :articles
end

API Versioning Strategies

Versioning ensures backward compatibility for API changes.

Example

# app/controllers/api/v1/base_controller.rb
module Api
module V1
class BaseController < ApplicationController
# Version 1 API logic
end
end
end
# app/controllers/api/v2/base_controller.rb
module Api
module V2
class BaseController < ApplicationController
# Version 2 API logic
end
end
end
# config/routes.rb
namespace :api do
namespace :v1 do
resources :users
end
namespace :v2 do
resources :users
end
end

Handling Background Jobs Effectively (Sidekiq Best Practices)

Sidekiq Configuration

# Gemfile
gem 'sidekiq'
# config/initializers/sidekiq.rb
Sidekiq.configure_server do |config|
config.redis = { url: 'redis://localhost:6379/0' }
end
Sidekiq.configure_client do |config|
config.redis = { url: 'redis://localhost:6379/0' }
end

Defining a Worker

# app/workers/my_worker.rb
class MyWorker
include Sidekiq::Worker
def perform(user_id)
user = User.find(user_id)
user.update(last_login: Time.current)
end
end

Scheduling Jobs

# app/jobs/daily_summary_job.rb
class DailySummaryJob < ApplicationJob
queue_as :default
def perform
User.send_daily_summaries
end
end
# Schedule with `whenever`
every :day, at: '12:00 am' do
runner "DailySummaryJob.perform_later"
end

Strategies for Breaking Monoliths into Microservices

Identifying Boundaries

  • Domain-Driven Design: Identify bounded contexts within your application.

Creating Microservices

  • Separate Codebases: Each microservice should have its own repository.
  • API Communication: Use HTTP/REST or gRPC for communication between services.

Example

# User Service (Microservice)
class UsersController < ApplicationController
def show
user = User.find(params[:id])
render json: user
end
end
# Order Service (Microservice)
class OrdersController < ApplicationController
def create
user_response = HTTP.get("http://user_service/users/#{params[:user_id]}")
user = JSON.parse(user_response.body)
# Create order logic
end
end

Feature Toggles and Canary Releases

Feature Toggles

Feature toggles allow you to enable or disable features without deploying new code.

Example with flipper Gem

# Gemfile
gem 'flipper'
# config/initializers/flipper.rb
Flipper.configure do |config|
config.default do
adapter = Flipper::Adapters::ActiveRecord.new
Flipper.new(adapter)
end
end
# Usage
if Flipper.enabled?(:new_feature)
# New feature logic
else
# Old feature logic
end

Canary Releases

Canary releases allow you to deploy features to a small subset of users before a full rollout.

Example

# config/initializers/canary_releases.rb
class CanaryRelease
def self.enabled_for?(user)
user.id % 10 == 0 # Enable for 10% of users
end
end
# Usage
if CanaryRelease.enabled_for?(current_user)
# New feature logic
else
# Old feature logic
end

By implementing these best practices and strategies, you can build scalable, maintainable, and high-performance Rails applications. Keep refactoring your architecture, modularizing your codebase, and leveraging advanced features of Rails to stay ahead in the rapidly evolving landscape of web development.

--

--

Bhavyansh @ DiversePixel
Bhavyansh @ DiversePixel

Written by Bhavyansh @ DiversePixel

Hey I write about Tech. Join me as I share my tech learnings and insights. 🚀

Responses (1)