Service objects in Rails

The path to slimmer controllers and models

Photo by Patrick Schneider on Unsplash
class UserController < ApplicationController
def create
user = User.new(user_params)
if user.save
send_welcome_email
notify_slack
if @user.admin?
log_new_admin
else
log_new_user
end
redirect_to new_user_welcome_path
else
render 'new'
end
end
# private methods
end

What are service objects?

class RegisterUser
def initialize(user)
@user = user
end
def execute
return nil unless @user.save
send_welcome_email
notify_slack
if @user.admin?
log_new_admin
else
log_new_user
end
@user
end
# private methods
end
class UserController < ApplicationController
def create
user = RegisterUser.new(User.new(user_params)).execute
if user
redirect_to new_user_welcome_path
else
render 'new'
end
end
# private methods
end

Returning values

class RegisterUser
def initialize(user)
@user = user
end
def execute
return OpenStruct.new(success?: false, user: nil, errors: @user.errors) unless @user.save
send_welcome_email
notify_slack
if @user.admin?
log_new_admin
else
log_new_user
end
OpenStruct.new(success?: true, user: @user, errors: nil)
end
# private methods
end
class UserController < ApplicationController
def create
result = RegisterUser.new(User.new(user_params)).execute
if result.success?
redirect_to new_user_welcome_path
else
render 'new', error: result.errors
end
end
# private methods
end

Syntactic sugar

class RegisterUser
def self.call(*args, &block)
new(*args, &block).execute
end
def initialize(user)
@user = user
end
def execute
# old code
end
# private methods
end
class UserController < ApplicationController
def create
result = RegisterUser.call(User.new(user_params))
if result.success?
redirect_to new_user_welcome_path
else
render 'new', error: result.errors
end
end
# private methods
end

Calling dependent services

class RegisterUser
def initialize(user)
@user = user
end
def execute
return OpenStruct.new(success?: false, user: nil, errors: @user.errors) unless @user.save
send_welcome_email
NotifySlack.call(@user)
if @user.admin?
log_new_admin
else
log_new_user
end
OpenStruct.new(success?: true, user: @user, errors: nil)
end
# private methods
end
class RegisterUser
def initialize(user)
@user = user
end
def execute
return OpenStruct.new(success?: false, user: nil, errors: @user.errors) unless @user.save
send_welcome_email
notify_slack_service.execute
if @user.admin?
log_new_admin
else
log_new_user
end
OpenStruct.new(success?: true, user: @user, errors: nil)
end
private def notify_slack_service
@notify_slack_service ||= NotifySlack.new(@user)
end
# private methods
end

What should be a service object?

Further reading

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Scott Domes

Writer & teacher, currently focused on software (JavaScript, Rails & more). Author of Progressive Web Apps with React.