What a service looks like A Servus service is a Ruby class that inherits from Servus::Base, defines initialize and call, and returns a Response.
ruby module Treasury
module TransferGold
class Service < Servus :: Base
# Validate arguments before call, results after
schema (
arguments: {
type: "object" ,
required: [ "from_account" , "to_account" , "gold_dragons" ],
properties: {
from_account: { type: [ "integer" , "object" ] },
to_account: { type: [ "integer" , "object" ] },
gold_dragons: { type: "integer" , minimum: 1 }
}
},
result: {
type: "object" ,
required: [ "transferred" , "from_balance" , "to_balance" ],
properties: {
transferred: { type: "number" },
from_balance: { type: "number" },
to_balance: { type: "number" }
}
}
)
# Fire an event for handlers to pick up
emits :gold_transferred , on: :success
# Accept an Account instance or an ID — resolve lazily
lazily :from_account , finds: Account
lazily :to_account , finds: Account
def initialize ( from_account: , to_account: , gold_dragons: )
@from_account = from_account
@to_account = to_account
@gold_dragons = gold_dragons
end
def call
# Business failure — returns a Response the caller can handle
return failure ( "Cannot transfer to the same account" ) if transferring_to_self?
# Guard — halts with a structured error if the precondition fails
enforce_eligible_transfer! (
from: from_account,
to: to_account,
amount: @gold_dragons
)
from_account. withdraw! (@gold_dragons)
to_account. deposit! (@gold_dragons)
# Success — wrapped in a DataObject with accessor methods
success (
transferred: @gold_dragons,
from_balance: from_account. balance ,
to_balance: to_account. balance
)
end
private
def transferring_to_self?
from_account == to_account
end
end
end
end What's happening here There's a lot on that page — here's what each piece does, with links to the relevant docs.
Piece What it does Docs < Servus::BaseThe base class. Wraps your call method with validation, logging, event dispatch, and error handling. Service Objects lazily :from_account, finds: AccountThe argument accepts an Account instance or an id; the service resolves whichever is passed. Lazy Resolvers schema(arguments:, result:)Optional JSON Schema validation for arguments (before call) and results (after). Here it rejects decimals and amounts below 1. Schema Validation emits :gold_transferred, on: :successFires an event on successful completion for handlers to pick up. Event Bus enforce_eligible_transfer!(...)A guard. Halts execution and returns a structured failure if the precondition isn't met. Guards failure(...) and success(...)The two ways a service returns. Both produce a Response the caller can branch on. Responses
Callers get a single, predictable shape:
ruby result = Treasury :: TransferGold :: Service . call ( from_account: 1 , to_account: 2 , gold_dragons: 50 )
puts result. success? # => true
puts result. data . transferred # => 50
puts result. data . from_balance # => 950
puts result. data . to_balance # => 550
# Business failure
result = Treasury :: TransferGold :: Service . call ( from_account: 1 , to_account: 1 , gold_dragons: 50 )
puts result. success? # => false
puts result. error . message # => "Cannot transfer to the same account" The same service runs inline through .call or through ActiveJob via .call_async .
How to read this The handbook moves outward from the basics:
Getting Started — a minimum service and the mental model behind itCore Concepts — services, the call chain, responses, architectureFeatures — schema validation, error handling, guards, events, async, lazy resolversRails Integration — controllers, generators, configuration, background jobsTesting — framework testing and the RSpec matchersReference — generators and dry-initializerThe running example Most examples come from RavenPay, a fictional treasury and messaging system. Using one running example keeps the vocabulary consistent across services, events, guards, and async work.