Rails Service Objects in 2026: Design Patterns, PORO en Technische Sollicitatievragen
Service Objects in Rails extraheren businesslogica uit controllers en models naar Plain Old Ruby Objects (POROs). Dit pattern houdt Rails-applicaties onderhoudbaar en wordt vaak getest in technische sollicitatiegesprekken.

Service Objects in Rails extraheren businesslogica uit controllers en models naar Plain Old Ruby Objects (POROs). Dit pattern houdt Rails-applicaties onderhoudbaar terwijl ze groeien, en interviewers testen het frequent om het begrip van een kandidaat van clean architecture te beoordelen.
Een Service Object kapselt één businessoperatie in. De klasse heeft één publieke methode (typisch call), accepteert dependencies via de constructor en retourneert een voorspelbaar resultaat. Kandidaten die kunnen uitleggen waarom dit belangrijk is – niet alleen hoe het te implementeren – vallen op.
Waarom Service Objects Fat Model en Fat Controller problemen oplossen
Rails moedigt aan om logica ergens te plaatsen. Zonder begeleiding duwen teams het in models ("fat models, skinny controllers") totdat ActiveRecord-klassen uitgroeien tot duizenden regels. Anderen laten het in controllers, waardoor actions onmogelijk geïsoleerd te testen zijn.
Service Objects bieden een derde weg: domeinlogica leeft in dedicated klassen die van niets uit Rails afhangen behalve wat ze expliciet nodig hebben. Deze ontkoppeling maakt de code testbaar zonder het volledige framework te laden, portable over verschillende ingangspunten (controllers, background jobs, console) en leesbaar omdat elke klasse één ding doet.
De Rails Guides schrijven geen Service Objects voor, maar de community heeft zich op het pattern gestandardiseerd. Rails 8 benadrukt eenvoud met Solid Queue en Solid Cache die externe dependencies vervangen, wat POROs nog aantrekkelijker maakt: minder gems, meer pure Ruby.
Anatomie van een productie-ready Service Object
Een Service Object heeft drie dingen nodig: een constructor die dependencies ontvangt, één publieke call-methode en een return type dat succes of falen signaleert.
# app/services/users/create_account.rb
module Users
class CreateAccount
def initialize(user_repo: User, mailer: UserMailer)
@user_repo = user_repo
@mailer = mailer
end
def call(params)
user = @user_repo.new(params)
return Result.failure(:validation_failed, user.errors) unless user.valid?
user.save!
@mailer.welcome_email(user).deliver_later
Result.success(user)
rescue ActiveRecord::RecordNotUnique
Result.failure(:email_taken, "Email already registered")
end
end
endDe constructor accepteert user_repo en mailer met standaardwaarden. Productiecode gebruikt de defaults; tests injecteren mocks. De call-methode valideert, persisteert, verstuurt een e-mail en wikkelt elk resultaat in een Result-object.
Een minimale Result-klasse bouwen zonder gems
Sommige teams grijpen direct naar dry-monads. De gem is solide, maar voegt een leercurve toe. Een 30-regels Result-klasse dekt de meeste behoeften:
# app/services/result.rb
class Result
attr_reader :value, :error, :code
def initialize(success:, value: nil, error: nil, code: nil)
@success = success
@value = value
@error = error
@code = code
end
def success? = @success
def failure? = !@success
def self.success(value) = new(success: true, value: value)
def self.failure(code, error = nil) = new(success: false, code: code, error: error)
def on_success
yield(value) if success?
self
end
def on_failure
yield(code, error) if failure?
self
end
endDeze Result-klasse ondersteunt chaining met on_success en on_failure, exposeert gestructureerde foutcodes en vereist geen dependencies. Controllers consumeren het schoon:
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def create
result = Users::CreateAccount.new.call(user_params)
result
.on_success { |user| redirect_to user, notice: "Account created" }
.on_failure { |code, error| render :new, status: :unprocessable_entity }
end
private
def user_params
params.require(:user).permit(:email, :password, :name)
end
endTeams die al het dry-rb ecosysteem gebruiken, of teams die Do-notatie nodig hebben voor het chainen van meerdere operaties, profiteren van dry-monads. De gem biedt Success, Failure en Maybe types met pattern matching ondersteuning in Ruby 3.x.
Naamgevingsconventies die intentie signaleren
Twee conventies domineren:
| Stijl | Voorbeeld | Wanneer gebruiken |
|---|---|---|
| Werkwoordzin | CreateAccount, SendInvoice, RefundPayment | Commando's die state wijzigen |
| Zelfstandig naamwoord met -or | AccountCreator, InvoiceSender, PaymentRefunder | Minder gebruikelijk, sommige teams prefereren het |
De werkwoordzinstijl leest natuurlijk: Users::CreateAccount.new.call(params). Het -or suffix werkt maar voegt lettergrepen toe zonder duidelijkheid.
Directorystructuur is ook belangrijk. Groeperen per domein houdt gerelateerde services bij elkaar:
app/services/
users/
create_account.rb
reset_password.rb
update_profile.rb
orders/
place_order.rb
cancel_order.rb
calculate_totals.rb
result.rbKlaar om je Ruby on Rails gesprekken te halen?
Oefen met onze interactieve simulatoren, flashcards en technische tests.
Dependency Injection voor testbare services
Hardcoded dependencies maken testen pijnlijk. Deze service kan niet getest worden zonder de database te raken en echte e-mails te versturen:
# Vermijd: hardcoded dependencies
class CreateAccount
def call(params)
user = User.create!(params) # Directe model-aanroep
UserMailer.welcome(user).deliver # Directe mailer-aanroep
end
endInjectie via de constructor lost dit op:
# Beter: injecteerbare dependencies
class CreateAccount
def initialize(user_repo: User, mailer: UserMailer)
@user_repo = user_repo
@mailer = mailer
end
def call(params)
user = @user_repo.create!(params)
@mailer.welcome(user).deliver_later
Result.success(user)
end
endTests substitueren nu fakes:
# spec/services/users/create_account_spec.rb
RSpec.describe Users::CreateAccount do
let(:fake_repo) { class_double(User) }
let(:fake_mailer) { class_double(UserMailer) }
let(:service) { described_class.new(user_repo: fake_repo, mailer: fake_mailer) }
it "returns success with valid params" do
user = instance_double(User, valid?: true)
allow(fake_repo).to receive(:new).and_return(user)
allow(user).to receive(:save!)
allow(fake_mailer).to receive_message_chain(:welcome_email, :deliver_later)
result = service.call(email: "test@example.com", password: "secure123")
expect(result).to be_success
end
endGeen database, geen e-mails, snelle uitvoering. De RSpec-documentatie behandelt class_double en instance_double voor type-geverifieerde mocks.
Sollicitatievragen over Rails Service Objects
Deze vragen verschijnen in mid-level en senior Rails-interviews. Bereid concrete antwoorden voor met codevoorbeelden.
V: Wanneer moet businesslogica in een model versus een Service Object leven?
Models bezitten validaties, associaties, scopes en single-record gedrag. Service Objects handelen operaties af die meerdere models overspannen, externe aanroepen vereisen of expliciete transactiegrenzen nodig hebben. Een User-model valideert het e-mailformaat; een CreateAccount-service maakt de gebruiker aan, verstuurt een welkomst-e-mail en provisioneert standaardinstellingen.
V: Hoe ga je om met fouten in Service Objects zonder exceptions?
Retourneer een Result-object met success/failure states. Dit maakt foutafhandeling expliciet in de aanroeper, vermijdt exception-gebaseerde control flow en biedt gestructureerde foutcodes voor verschillende faalscenario's. Exceptions blijven gepast voor echt uitzonderlijke condities (database down, netwerkfout) in plaats van businessregelschendingen.
V: Wat is het verschil tussen een Service Object en een Interactor?
Interactors (van gems zoals interactor) zijn Service Objects met een specifieke interface: ze ontvangen een context-hash, muteren deze en signaleren falen via context.fail!. Service Objects als POROs hebben geen voorgeschreven interface, wat teams meer flexibiliteit maar minder consistentie geeft.
V: Hoe test je een Service Object dat externe APIs aanroept?
Injecteer de HTTP-client als dependency. In tests, geef een stub door die voorgedefinieerde responses retourneert. Tools zoals WebMock of VCR kunnen HTTP-interacties opnemen en afspelen, maar constructor-injectie vermijdt netwerkaanroepen volledig in unit tests.
Voor meer Rails-sollicitatievoorbereiding, zie de Rails sollicitatievragen gids over RSpec en testing patterns.
Railway-Oriented Design voor complexe workflows
Railway-oriented programming behandelt een workflow als twee parallelle sporen: succes gaat vooruit, falen stopt onmiddellijk. Elke stap gaat ofwel verder op het successpoor of schakelt naar het faalspoor.
# app/services/orders/place_order.rb
module Orders
class PlaceOrder
def initialize(
inventory: InventoryService.new,
payment: PaymentService.new,
fulfillment: FulfillmentService.new
)
@inventory = inventory
@payment = payment
@fulfillment = fulfillment
end
def call(cart:, payment_method:)
validate_cart(cart)
.then { |items| reserve_inventory(items) }
.then { |reservation| charge_payment(reservation, payment_method) }
.then { |charge| create_fulfillment(charge) }
end
private
def validate_cart(cart)
return Result.failure(:empty_cart, "Cart is empty") if cart.items.empty?
Result.success(cart.items)
end
def reserve_inventory(items)
@inventory.reserve(items)
end
def charge_payment(reservation, payment_method)
result = @payment.charge(reservation.total, payment_method)
return result if result.failure?
Result.success({ reservation: reservation, charge: result.value })
end
def create_fulfillment(data)
@fulfillment.create(data[:reservation], data[:charge])
end
end
endDe Result-klasse heeft een then-methode nodig die alleen doorgaat bij succes:
# Toevoegen aan app/services/result.rb
def then
return self if failure?
yield(value)
endAls de inventarisreservering faalt, draait de betaling nooit. Als de betaling faalt, draait de fulfillment nooit. Elke stap retourneert een Result, en de keten breekt af bij de eerste fout.
Wanneer Service Objects een anti-pattern worden
Service Objects lossen echte problemen op maar kunnen onnodig prolifereren. Let op deze waarschuwingssignalen:
-
Single-line services: Als de service alleen één model-methode aanroept, voegt de service indirectie toe zonder waarde.
User.authenticate(email, password)is beter danAuthenticateUser.new.call(email, password)wanneer de logica eenvoudig is. -
Services die alleen voor testen bestaan: Dependencies injecteren maakt testen makkelijker, maar services maken alleen om model-methodes te wrappen voor testbaarheid suggereert dat het echte probleem de test setup is, niet de architectuur.
-
Anemische services zonder logica: Services die alleen delegeren naar models zonder gedrag toe te voegen zijn ceremonie. De Netguru engineering blog bespreekt dit over-extractie probleem.
Het alternatief: gebruik POROs selectief naast Concerns voor gedeeld model-gedrag, Value Objects voor domeinconcepten zoals Money of DateRange, en model-methodes voor single-record operaties.
Kernpunten voor Rails Service Object Design
- Extraheer naar een Service Object wanneer logica meerdere models overspant, externe aanroepen vereist of expliciete foutafhandeling nodig heeft
- Gebruik één publieke
call-methode die een Result-object retourneert, geen ruwe waarden of exceptions - Injecteer dependencies via de constructor met verstandige defaults voor productiegebruik
- Benoem services met werkwoordzinnen zoals
CreateAccountofPlaceOrderdie de actie beschrijven - Organiseer services per domein (
users/,orders/) in plaats van een platte directorystructuur - Begin met een minimale Result-klasse voordat gems zoals dry-monads worden toegevoegd
- Reserveer exceptions voor infrastructuurfalen, niet voor businessregelschendingen
- Vermijd het maken van services voor single-line operaties die models natuurlijk afhandelen
Begin met oefenen!
Test je kennis met onze gespreksimulatoren en technische tests.
Zie jij de bug in Ruby on Rails?
Een echt codefragment, een verborgen bug, één poging per dag. Zonder account uit te proberen.

Geschreven door
Anthony Fillion-MailletOprichter van SharpSkill
Al meer dan 10 jaar fullstack-ontwikkelaar. Hij leidt SharpSkill en staat in voor alles wat hier verschijnt.
Bijgewerkt op 20 augustus 2026
Delen
Gerelateerde artikelen

Rails GraphQL API in 2026: graphql-ruby, Subscriptions en Sollicitatievragen
Een productieklare GraphQL API bouwen met Rails 8 en graphql-ruby. Schema-ontwerp, mutations, subscriptions met ActionCable en voorbereiding op sollicitatiegesprekken.

Rails Background Jobs in 2026: Sidekiq vs Good Job Vergelijking met Sollicitatievragen
Uitgebreide vergelijking van Sidekiq, Good Job en Solid Queue voor Rails background jobs. Handleiding voor het kiezen van de juiste oplossing met typische technische sollicitatievragen.

Rails Active Storage in 2026: Bestandsuploads, S3-Integratie en Sollicitatievragen
Beheers Rails Active Storage voor bestandsuploads met S3 en directe uploads. Complete tutorial met codevoorbeelden en veelgestelde sollicitatievragen over bestandsafhandeling in Ruby on Rails.