Skip to content

Patterns in Ruby: Service object #101

@joshmfrankel

Description

@joshmfrankel

The Service Object Pattern

Description

  • Change Something / Return Nothing
  • Perform a single unit of business logic
  • File name describes the side effect

When to use it

  • You have a single contained unit of business logic
  • You need to execute a series of steps
  • You want to initiate a side-effect

When not to use it (Anti-patterns)

  • You have made all objects into Services - Everything is a Service (spoiler: it isn't)
  • You are mixing querying and command pattern
  • You have service objects that are actually multiple service objects (God Objects)

Before

class UsersController < ApplicationController
  def create
    user = User.new(user_params)

    if user.save
      UserMailer.sign_up.deliver_later
      
      Subscription.create(status: "active", user:)
      Analytics::UserSignUpEvent.track(user)

      head 200
    else
      head 400
    end
  end
end

After

class CreateUserService < ApplictionService

  def initialize(user:)
    @user = user
  end

  def call
    User.transaction do
      user.save

      UserMailer.sign_up.deliver_later
      Subscription.create(status: "active", user:)
      Analytics::UserSignUpEvent.track(user)
    end
    
    true
  rescue SubscriptionError, UserError => e
    false
  end

  private

  attr_reader :user
end

class UsersController < ApplicationController
  def create
    user = User.new(user_params)
    service = CreateUserService.new(user:)

    if service.call
      head 200
    else
      head 400
    end
  end
end

Future

Result returning

class CreateUserService < ApplictionService

  def initialize(user:)
    @user = user
  end

  def call
    User.transaction do
      user.save

      UserMailer.sign_up.deliver_later
      Subscription.create(status: "active", user:)
      Analytics::UserSignUpEvent.track(user)
    end
    
    Success.new(user, "User created")
  rescue SubscriptionError, UserError => exception
    Failure.new(exception)
  end

  private

  attr_reader :user
end

class UsersController < ApplicationController
  def create
    user = User.new(user_params)
    service = CreateUserService.new(user:)
    service.call

    if service.success?
      render json: { error: service.message }
    else
      render json: { error: service.exception }
    end
  end
end

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions