35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English 35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English
Rails Service Objects: Patronen Die Echt Werken in Productie

Rails Service Objects: Patronen Die Echt Werken in Productie

TTB Software
rails
Hoe je Rails service objects bouwt die onderhoudbaar blijven. Echte patronen, naamgevingsconventies, foutafhandeling en teststrategieën uit productiecodebases.

Je Rails controllers worden te dik. Je models weten te veel. Je hebt gelezen dat service objects de oplossing zijn, maar elk blogartikel toont een ander patroon en geen ervan behandelt wat er gebeurt wanneer je service een andere service aanroept die een derde service aanroept en halverwege iets misgaat.

Dit is wat daadwerkelijk werkt na jarenlang service objects in productie Rails-apps (Rails 7.1+ en 8.0) te draaien.

Wat een Service Object Eigenlijk Is

Een service object is een gewone Ruby-class die verantwoordelijk is voor één bedrijfsoperatie. Het neemt input, doet één ding, en geeft een resultaat terug. Dat is alles.

Het sleutelwoord is operatie, niet ding. UserCreator is een service. UserHelper niet — dat is gewoon een rommella met een classnaam.

# app/services/users/create.rb
module Users
  class Create
    def initialize(params:, invited_by: nil)
      @params = params
      @invited_by = invited_by
    end

    def call
      user = User.new(@params)

      ActiveRecord::Base.transaction do
        user.save!
        Onboarding::SendWelcomeEmail.new(user: user).call
        track_referral(user) if @invited_by
      end

      Result.success(user)
    rescue ActiveRecord::RecordInvalid => e
      Result.failure(e.record.errors)
    end

    private

    def track_referral(user)
      Referrals::Track.new(referrer: @invited_by, referred: user).call
    end
  end
end

Dit handelt gebruikersaanmaak, welkomstmail en referral-tracking af in één transactie. De controller blijft dun:

class UsersController < ApplicationController
  def create
    result = Users::Create.new(params: user_params).call

    if result.success?
      redirect_to dashboard_path
    else
      @errors = result.errors
      render :new, status: :unprocessable_entity
    end
  end
end

Het Result Object Patroon

Elke service moet succes of falen communiceren. true/false retourneren werkt tot je foutmeldingen nodig hebt. Exceptions gooien werkt tot je verwachte fouten netjes wilt afhandelen. Een simpel result object lost beide op:

# app/services/result.rb
class Result
  attr_reader :value, :errors

  def initialize(success:, value: nil, errors: nil)
    @success = success
    @value = value
    @errors = errors
  end

  def success? = @success
  def failure? = !@success

  def self.success(value = nil)
    new(success: true, value: value)
  end

  def self.failure(errors)
    new(success: false, errors: Array(errors))
  end
end

Dertig regels. Geen gems. De code is van jou. Ik heb teams gezien die dry-monads of interactor installeerden hiervoor en meer tijd besteedden aan het leren van de gem-API dan aan het schrijven van bedrijfslogica.

Naamgeving en Bestandsorganisatie

Zet services in app/services/, genamespaced per domein:

app/services/
├── result.rb
├── users/
│   ├── create.rb
│   ├── deactivate.rb
│   └── merge_accounts.rb
├── orders/
│   ├── place.rb
│   ├── cancel.rb
│   └── refund.rb
└── reports/
    ├── generate_monthly.rb
    └── export_csv.rb

Noem services met werkwoorden, niet zelfstandige naamwoorden. Orders::Place, niet OrderPlacer. Users::Deactivate, niet UserDeactivationService. Het Service-achtervoegsel is overbodig — alles in app/services/ is een service.

Rails autoloading pikt app/services/ standaard op in Rails 7+ met Zeitwerk. Geen configuratie nodig.

Wanneer een Service Object Extraheren

Niet alles heeft een service nodig. Dit is het besliskader dat ik gebruik:

Extraheer naar een service wanneer:

  • De operatie meerdere models of externe systemen omvat
  • De logica niet bij één enkel model hoort
  • Dezelfde operatie vanuit meerdere plekken wordt aangeroepen (controller, achtergrondtaak, console)
  • De operatie complexe foutafhandeling of rollback-logica heeft

Houd het in het model wanneer:

  • Het een validatie of callback voor één model is
  • Het een query scope is
  • Het een simpele berekening op modelattributen is

Houd het in de controller wanneer:

  • Het alleen parameterfiltering en een enkele model-save is
  • Er geen bedrijfslogica is buiten CRUD

De slechtste service objects die ik heb gezien zijn degene die User.create(params) in een class wrappen. Dat is geen abstractie — dat is indirectie.

Foutafhandeling Die Niet Instort

Het lastige deel is service-compositie — wanneer services andere services aanroepen.

Probleem: Exceptions Die Doorvallen

Alleen de exceptions rescuen die je verwacht. Laat bugs omhoog bubbelen. Gebruik ActiveRecord::Rollback voor gecontroleerde transactiefouten.

Probleem: Niet-Kritieke Fouten Blokkeren Kritieke Operaties

Soms roept een service een andere service aan die niet essentieel is. Een welkomstmail die faalt zou gebruikersaanmaak niet moeten tegenhouden. Duw niet-kritiek werk naar een achtergrondtaak:

def call
  user = User.new(@params)
  user.save!
  OnboardingMailer.welcome(user).deliver_later
  Result.success(user)
rescue ActiveRecord::RecordInvalid => e
  Result.failure(e.record.errors.full_messages)
end

Service Objects Testen

Services zijn makkelijk te testen omdat het gewone Ruby is. Geen controller-context, geen view-rendering:

# test/services/users/create_test.rb
require "test_helper"

class Users::CreateTest < ActiveSupport::TestCase
  test "creates user with valid params" do
    result = Users::Create.new(
      params: { email: "test@example.com", name: "Test User", password: "secure123" }
    ).call

    assert result.success?
    assert_equal "test@example.com", result.value.email
  end

  test "returns failure with invalid params" do
    result = Users::Create.new(params: { email: "" }).call

    assert result.failure?
    assert_includes result.errors.first, "Email"
  end
end

Test de publieke interface: call met inputs, assert op het resultaat. Test geen private methods direct.

Veelvoorkomende Fouten

De God Service. Een service die tien dingen doet is geen service — het is een procedure in een class-kostuum. Als je call-methode meer dan 30 regels is, splits het op.

Internals testen. Mock reserve_inventory! en process_payment! niet apart. Test de service end-to-end.

Gemixte returntypes. Als call soms een User retourneert, soms nil, en soms een exception gooit — kies een patroon. Het Result object elimineert deze hele klasse van bugs.

De transactie overslaan. Als je service naar meerdere tabellen schrijft, wrap het in een transactie.

Veelgestelde Vragen

Moet ik een gem zoals Interactor of Trailblazer gebruiken voor service objects?

Voor de meeste Rails-apps zijn gewone Ruby-classes met een Result object genoeg. Interactor voegt een context-object en hooks toe die koppeling creëren tussen stappen. Trailblazer is een volledig architectuurframework — nuttig voor complexe projecten, maar zwaar voor typische CRUD-apps. Begin simpel.

Hoe verhouden service objects zich tot Rails concerns?

Ze lossen verschillende problemen op. Concerns delen gedrag tussen models (zoals Taggable of Searchable). Service objects kapselen bedrijfsoperaties in die meerdere models of externe systemen omvatten. Een concern zou geen service moeten aanroepen, en een service zou geen concern moeten includen.

Moeten service objects vanuit model-callbacks worden aangeroepen?

Nee. Model-callbacks horen modelinterne consistentie te bewaken. Services aanroepen vanuit callbacks creëert verborgen bijeffecten die debugging pijnlijk en testen fragiel maken. Trigger services vanuit controllers of achtergrondtaken waar de intentie expliciet is.

Hoe ga ik om met autorisatie in service objects?

Geef de huidige gebruiker mee als argument en controleer permissies vroeg. Gebruik geen Current.user binnen services — het creëert een impliciete afhankelijkheid die breekt in achtergrondtaken en tests.

Wat is het verschil tussen een service object en een form object?

Een form object handelt inputvalidatie en -coercie af voor een specifiek formulier. Een service object handelt bedrijfslogica af voor een specifieke operatie. Ze kunnen samenwerken: de controller geeft gevalideerde formulierdata door aan een service.

T

About the Author

Roger Heykoop is een senior Ruby on Rails ontwikkelaar met 19+ jaar Rails ervaring en 35+ jaar ervaring in softwareontwikkeling. Hij is gespecialiseerd in Rails modernisering, performance optimalisatie, en AI-ondersteunde ontwikkeling.

Get in Touch

Share this article

Need Expert Rails Development?

Let's discuss how we can help you build or modernize your Rails application with 19+ years of expertise

Schedule a Free Consultation