Result Object Pattern
Result Object Pattern
Summary
A Result Object is a small object returned by a method or service to clearly communicate whether an operation succeeded or failed.
Instead of returning random values like true, false, nil, a hash, or raising exceptions for normal business failures, the method returns a structured object.
The Result Object usually answers:
- Did the operation succeed?
- Did it fail?
- What value was produced?
- What error happened?
Example:
result = CreateUser.call(params)
if result.success?
redirect_to user_path(result.value)
else
flash[:alert] = result.error
render :new
end
Is Result built into Ruby or Rails?
No.
Ruby and Rails do not include a generic Result object by default.
When developers use the Result Object pattern, they usually either:
- Define their own
Resultclass. - Use a gem that provides similar behavior, such as
dry-monads.
For learning Rails, it is usually better to create a simple Result class manually first.
Problem It Solves
Without a Result Object, service objects may return inconsistent values.
For example:
user = CreateUser.call(params)
if user
# success
else
# failure, but why?
end
The problem is that nil or false does not explain what went wrong.
Another common approach is returning hashes:
response = CreateUser.call(params)
if response[:success]
user = response[:user]
else
error = response[:error]
end
This works, but it is less explicit. The structure is informal, and different services may use different keys.
A Result Object creates a consistent contract.
Instead of guessing what the method returned, the caller can always ask:
result.success?
result.failure?
result.value
result.error
Why It Exists
The pattern exists because not every failure should be treated as an exception.
Some failures are expected business outcomes.
Examples:
- Email already taken
- Payment declined
- User not authorized
- Record validation failed
- Planned expense already executed
- Not enough inventory
- External API rejected the request
These are not always bugs. They are normal possibilities in the domain.
Exceptions should usually be reserved for unexpected technical problems, such as:
- Database connection failed
- Missing constant
- Programming bug
- Unexpected nil value
- External service timeout, depending on context
A Result Object gives the application a clear way to handle expected success and failure states.
Simple Ruby Implementation
# app/lib/result.rb
class Result
attr_reader :value, :error
def self.success(value = nil)
new(success: true, value: value)
end
def self.failure(error)
new(success: false, error: error)
end
def initialize(success:, value: nil, error: nil)
@success = success
@value = value
@error = error
end
def success?
@success
end
def failure?
!success?
end
end
Example in a Rails Service Object
class PlannedExpenses::ExecuteService
def self.call(planned_expense:)
new(planned_expense: planned_expense).call
end
def initialize(planned_expense:)
@planned_expense = planned_expense
end
def call
return Result.failure("Already executed") if @planned_expense.executed?
@planned_expense.update!(status: "executed")
Result.success(@planned_expense)
rescue ActiveRecord::RecordInvalid => error
Result.failure(error.record.errors.full_messages.to_sentence)
end
end
Example in a Rails Controller
result = PlannedExpenses::ExecuteService.call(
planned_expense: @planned_expense
)
if result.success?
redirect_to finance_path, notice: "Expense executed successfully"
else
redirect_to finance_path, alert: result.error
end
The controller does not need to know all the internal details of the service.
It only needs to know:
result.success?
result.failure?
result.value
result.error
Why This Makes Code Cleaner
The Result Object pattern helps make code:
- More predictable
- Easier to test
- Easier to read
- Easier to refactor
- Less dependent on exceptions for normal business logic
- More consistent across service objects
Instead of every service inventing its own return style, all services can follow the same contract.
Result Object vs Exceptions
Use a Result Object for expected business outcomes.
Example:
return Result.failure("User is not authorized")
Use exceptions for unexpected problems.
Example:
raise "Unexpected missing account"
A good mental model:
Expected failure: return a Result.
Unexpected failure: raise an exception.
Result Object vs ActiveRecord Validation
Rails models already have a built-in validation flow:
user.save
user.errors.full_messages
For simple model saves, a Result Object may not be necessary.
But for service objects that coordinate multiple steps, a Result Object becomes more useful.
Example services:
CreateOrderRegisterWorkerExecutePlannedExpenseSyncTwilioCallsChargeCustomerImportCsvCreateBooking
These operations can have several success and failure paths, so a Result Object helps organize the response.
When To Use It
Use a Result Object when:
- A service can succeed or fail in expected ways.
- The caller needs to know why the operation failed.
- The operation has business rules.
- The operation coordinates multiple models or external services.
- You want a consistent return contract across services.
Avoid it when:
- The method is very small.
- Rails validations are already enough.
- The extra abstraction does not improve clarity.
Tiny Memory Hook
A Result Object is like a formal answer from a service:
Success: here is the value.
Failure: here is the reason.
The caller should not have to guess what happened.
Personal Rails Rule
For Rails service objects, prefer returning a Result Object when the service represents an important business action.
Example:
Result.success(record)
Result.failure("Already executed")
Result.failure("User is not allowed")
Result.failure(record.errors.full_messages.to_sentence)
This makes the code feel more senior because the service has a clear and predictable contract .