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
endBackground 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.ymlActionMailer
# 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 # syncCaching
# 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-ModifiedActionCable (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