Ruby
06 / 07

Rails Advanced

Ruby: Rails Advanced

Service Objects

Service objects encapsulate business logic that doesn't belong in models or controllers. They keep controllers thin and models focused on persistence.

# app/services/create_post_service.rb
class CreatePostService
  Result = Struct.new(:success?, :post, :errors, keyword_init: true)

  def initialize(user:, params:)
    @user = user
    @params = params
  end

  def call
    post = @user.posts.build(@params)
    post.slug = SlugGenerator.call(post.title)

    if post.save
      NotifyFollowersJob.perform_later(post)
      Result.new(success?: true, post: post, errors: [])
    else
      Result.new(success?: false, post: post, errors: post.errors.full_messages)
    end
  end
end

# In controller
result = CreatePostService.new(user: current_user, params: post_params).call
if result.success?
  redirect_to result.post
else
  @errors = result.errors
  render :new
end

Background Jobs (Active Job + Sidekiq)

# Gemfile
# gem 'sidekiq'

# config/application.rb
config.active_job.queue_adapter = :sidekiq

# app/jobs/send_welcome_email_job.rb
class SendWelcomeEmailJob < ApplicationJob
  queue_as :default
  retry_on Net::SMTPError, wait: :polynomially_longer, attempts: 5
  discard_on ActiveRecord::RecordNotFound

  def perform(user_id)
    user = User.find(user_id)
    UserMailer.welcome(user).deliver_now
  end
end

# Enqueue
SendWelcomeEmailJob.perform_later(user.id)          # async
SendWelcomeEmailJob.set(wait: 1.hour).perform_later(user.id)
SendWelcomeEmailJob.perform_now(user.id)            # sync (testing)

# config/sidekiq.yml
# :queues:
#   - [critical, 3]
#   - [default, 2]
#   - [low, 1]

# sidekiq -C config/sidekiq.yml

ActionMailer

# app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
  default from: 'noreply@myapp.com'

  def welcome(user)
    @user = user
    @login_url = login_url
    mail(to: @user.email, subject: 'Welcome to MyApp!')
  end

  def password_reset(user, token)
    @user = user
    @reset_url = password_reset_url(token: token)
    mail(to: @user.email, subject: 'Reset your password')
  end
end

# app/views/user_mailer/welcome.html.erb
# <h1>Hi <%= @user.name %></h1>

# Sending
UserMailer.welcome(@user).deliver_later   # via Active Job (async)
UserMailer.welcome(@user).deliver_now     # sync

Caching

# config/environments/production.rb
config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'] }
config.action_controller.perform_caching = true

# Fragment caching in views (Russian doll — nested caches)
# <%= cache @post do %>
#   <h1><%= @post.title %></h1>
#   <% @post.comments.each do |comment| %>
#     <%= cache comment do %>
#       <%= comment.body %>
#     <% end %>
#   <% end %>
# <% end %>

# Low-level caching in Ruby code
class PostsController < ApplicationController
  def expensive_stats
    @stats = Rails.cache.fetch('posts/stats', expires_in: 1.hour) do
      Post.expensive_calculation   # only runs on cache miss
    end
  end
end

# Action caching (whole response) — use HTTP caching instead
# expires_in 1.hour, public: true
# fresh_when(@post)   # ETag + Last-Modified

ActionCable (WebSockets)

# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
  def subscribed
    stream_from "chat_#{params[:room]}"
  end

  def unsubscribed
    # cleanup
  end

  def speak(data)
    ActionCable.server.broadcast("chat_#{params[:room]}", {
      message: data['message'],
      user: current_user.name
    })
  end
end

# Broadcast from anywhere (e.g., a job)
ActionCable.server.broadcast('chat_general', { message: 'Hello!' })

# JavaScript client
# const cable = createConsumer()
# const channel = cable.subscriptions.create(
#   { channel: 'ChatChannel', room: 'general' },
#   { received(data) { appendMessage(data) } }
# )

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free