Steps are the core components of a service, each representing a unit of work executed in sequence when the service is called.
- Define steps using the
stepkeyword within the service class - Use
ifandunlessoptions for conditional steps - Inherit steps from parent classes
- Inject steps into the execution flow with
beforeandafteroptions - Ensure steps always run with the
always: trueoption - Retry steps with the
retryoption [In Development]
class GeneralParserService < ApplicationService
step :create_browser, unless: :browser
step :parse_content
step :quit_browser, always: true
end
class ParsePage < GeneralParserService
step :parse_additional_content, after: :parse_content
endSteps are declared using the step keyword in your service class.
class User::Charge < ApplicationService
step :authorize
step :charge
step :send_email_receipt
private
def authorize
# ...
end
def charge
# ...
end
def send_email_receipt
# ...
end
endSteps can be conditional, executed based on specified conditions using the if or unless keywords.
class User::Charge < ApplicationService
step :authorize
step :charge
step :send_email_receipt, if: :send_receipt?
# ...
def send_receipt?
rand(2).zero?
end
endThis feature works well with argument predicates.
class User::Charge < ApplicationService
arg :send_receipt, type: :boolean, default: true
step :send_email_receipt, if: :send_receipt?
# ...
endYou can also use Procs (lambdas) for inline conditions:
class User::Charge < ApplicationService
arg :amount, type: :float
step :apply_discount, if: -> { amount > 100 }
step :charge
step :send_large_purchase_alert, if: -> { amount > 1000 }
# ...
end{% hint style="info" %} Using Procs can make simple conditions more readable, but for complex logic, prefer extracting to a method. {% endhint %}
Steps are inherited from parent classes, making it easy to build upon existing services.
# UpdateRecordService
class UpdateRecordService < ApplicationService
arg :record, type: ApplicationRecord
arg :attributes, type: Hash
step :authorize
step :update_record
end# User::Update inherited from UpdateRecordService
class User::Update < UpdateRecordService
# Arguments and steps are inherited from UpdateRecordService
endSteps can be injected at specific points in the execution flow using before and after options.
Let's enhance the previous example by adding a step to send a notification after updating the record.
# User::Update inherited from UpdateRecordService
class User::Update < UpdateRecordService
step :log_action, before: :authorize
step :send_notification, after: :update_record
private
def log_action
# ...
end
def send_notification
# ...
end
endCombine this with if and unless options for more control.
step :send_notification, after: :update_record, if: :send_notification?{% hint style="info" %}
By default, if neither before nor after is specified, the step is added at the end of the execution flow.
{% endhint %}
To ensure certain steps run regardless of previous step outcomes, use the always: true option. This is particularly useful for cleanup tasks, error logging, etc.
class ParsePage < ApplicationService
arg :url, type: :string
step :create_browser
step :parse_content
step :quit_browser, always: true
private
attr_accessor :browser
def create_browser
self.browser = Watir::Browser.new
end
def parse_content
# ...
end
def quit_browser
browser&.quit
end
endUse done! to stop executing remaining steps without adding an error. This is useful when you've completed the service's goal early and don't need to run subsequent steps.
class User::FindOrCreate < ApplicationService
arg :email, type: :string
step :find_existing_user
step :create_user
step :send_welcome_email
output :user
private
def find_existing_user
self.user = User.find_by(email:)
done! if user # Skip remaining steps if user already exists
end
def create_user
self.user = User.create!(email:)
end
def send_welcome_email
# Only runs for newly created users
Mailer.welcome(user).deliver_later
end
endYou can check if done! was called using done?:
def some_step
done!
# This code still runs within the same step
puts "Done? #{done?}" # => "Done? true"
end
def next_step
# This step will NOT run because done! was called
end{% hint style="info" %}
done! only stops subsequent steps from running. Code after done! within the same step method will still execute.
{% endhint %}
When inheriting from a parent service, you can remove steps using remove_step:
class UpdateRecordService < ApplicationService
step :authorize
step :validate
step :update_record
step :send_notification
end
class InternalUpdate < UpdateRecordService
# Remove authorization for internal system updates
remove_step :authorize
remove_step :send_notification
end{% hint style="warning" %} This feature is planned for a future release and is not yet implemented. {% endhint %}
Steps will be able to be retried a specified number of times before giving up, using the retry option.
class ParsePage < ApplicationService
step :parse_head, retry: true # Retry 3 times by default, no delay between retries
step :parse_body, retry: { times: 3, delay: 1.second }
endNext step is to learn about outputs. Outputs are the results of a service, returned upon completion of service execution.