Rails Service Objects di 2026: Design Patterns, PORO dan Pertanyaan Interview Teknis
Kuasai service objects di Rails dengan pola PORO, Result monad, dan clean architecture. Dilengkapi pertanyaan interview nyata dan contoh kode production-ready untuk Rails 8.

Service objects di Rails mengekstrak business logic dari controllers dan models ke dalam Plain Old Ruby Objects (POROs). Pola ini menjaga aplikasi Rails tetap maintainable seiring pertumbuhannya, dan interviewer sering mengujinya untuk menilai pemahaman kandidat tentang clean architecture.
Service object mengenkapsulasi satu operasi bisnis. Class tersebut memiliki satu method publik (biasanya call), menerima dependencies melalui constructor, dan mengembalikan result yang dapat diprediksi. Kandidat yang bisa menjelaskan mengapa ini penting, bukan hanya cara mengimplementasikannya, akan lebih menonjol.
Mengapa Service Objects Menyelesaikan Masalah Fat Model dan Fat Controller
Rails mendorong penempatan logic di suatu tempat. Tanpa panduan, tim memasukkannya ke models ("fat models, skinny controllers") sampai class ActiveRecord membengkak menjadi ribuan baris. Yang lain membiarkannya di controllers, membuat actions tidak mungkin ditest secara terpisah.
Service objects menawarkan jalur ketiga: domain logic berada di class-class khusus yang tidak bergantung pada apapun dari Rails kecuali yang secara eksplisit diperlukan. Decoupling ini membuat kode dapat ditest tanpa memuat seluruh framework, portable di berbagai entry points (controllers, background jobs, console), dan readable karena setiap class melakukan satu hal.
Rails Guides tidak meresepkan service objects, tetapi komunitas telah menstandarkan pola ini. Penekanan Rails 8 pada simplicity, dengan Solid Queue dan Solid Cache menggantikan external dependencies, membuat POROs semakin menarik: lebih sedikit gems, lebih banyak plain Ruby.
Anatomi Service Object yang Production-Ready
Service object membutuhkan tiga hal: constructor yang menerima dependencies, satu method publik call, dan return type yang menandakan success atau failure.
# 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
endConstructor menerima user_repo dan mailer dengan nilai default. Kode production menggunakan defaults; tests meng-inject mocks. Method call memvalidasi, menyimpan, mengirim email, dan membungkus setiap outcome dalam object Result.
Membangun Class Result Minimal Tanpa Gems
Beberapa tim langsung menggunakan dry-monads. Gem ini solid, tetapi menambah learning curve. Class Result 30 baris mencakup sebagian besar kebutuhan:
# 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
endResult ini mendukung chaining dengan on_success dan on_failure, mengekspos structured error codes, dan tidak memerlukan dependencies. Controllers mengkonsumsinya dengan bersih:
# 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
endTim yang sudah menggunakan ekosistem dry-rb, atau yang membutuhkan notasi Do untuk chaining multiple operations, akan mendapat manfaat dari dry-monads. Gem ini menyediakan tipe Success, Failure, dan Maybe dengan dukungan pattern matching di Ruby 3.x.
Konvensi Penamaan yang Menandakan Intent
Dua konvensi mendominasi:
| Style | Contoh | Kapan digunakan |
|---|---|---|
| Verb phrase | CreateAccount, SendInvoice, RefundPayment | Commands yang mengubah state |
| Noun dengan -or | AccountCreator, InvoiceSender, PaymentRefunder | Kurang umum, beberapa tim lebih suka |
Style verb phrase terbaca natural: Users::CreateAccount.new.call(params). Suffix -or berfungsi tetapi menambah suku kata tanpa kejelasan.
Struktur direktori juga penting. Pengelompokan berdasarkan domain menjaga services yang terkait tetap bersama:
app/services/
users/
create_account.rb
reset_password.rb
update_profile.rb
orders/
place_order.rb
cancel_order.rb
calculate_totals.rb
result.rbSiap menguasai wawancara Ruby on Rails Anda?
Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.
Dependency Injection untuk Services yang Testable
Hardcoded dependencies membuat testing menyakitkan. Service ini tidak bisa ditest tanpa mengakses database dan mengirim email sungguhan:
# Hindari: hardcoded dependencies
class CreateAccount
def call(params)
user = User.create!(params) # Direct model call
UserMailer.welcome(user).deliver # Direct mailer call
end
endInjection melalui constructor menyelesaikan ini:
# Lebih baik: injectable 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 sekarang dapat mengganti dengan 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
endTanpa database, tanpa emails, eksekusi cepat. Dokumentasi RSpec membahas class_double dan instance_double untuk type-verified mocks.
Pertanyaan Interview tentang Rails Service Objects
Pertanyaan-pertanyaan ini muncul dalam interview Rails tingkat menengah dan senior. Siapkan jawaban konkret dengan contoh kode.
T: Kapan business logic sebaiknya berada di model versus service object?
Models memiliki validations, associations, scopes, dan single-record behavior. Service objects menangani operasi yang melintasi multiple models, memerlukan external calls, atau membutuhkan explicit transaction boundaries. Model User memvalidasi format email; service CreateAccount membuat user, mengirim welcome email, dan menyediakan default settings.
T: Bagaimana menangani errors di service objects tanpa exceptions?
Kembalikan object Result dengan states success/failure. Ini membuat error handling eksplisit di caller, menghindari exception-based control flow, dan menyediakan structured error codes untuk berbagai failure modes. Exceptions tetap appropriate untuk kondisi yang benar-benar exceptional (database down, network failure) bukan business rule violations.
T: Apa perbedaan antara service object dan interactor?
Interactors (dari gems seperti interactor) adalah service objects dengan interface spesifik: mereka menerima hash context, memutasinya, dan menandakan failure melalui context.fail!. Service objects sebagai POROs tidak memiliki interface yang ditentukan, memberikan tim lebih banyak fleksibilitas tetapi kurang konsistensi.
T: Bagaimana cara mentest service object yang memanggil external APIs?
Inject HTTP client sebagai dependency. Dalam tests, berikan stub yang mengembalikan canned responses. Tools seperti WebMock atau VCR dapat merekam dan memutar ulang HTTP interactions, tetapi constructor injection menghindari network calls sepenuhnya dalam unit tests.
Untuk lebih banyak persiapan interview Rails, lihat panduan pertanyaan interview Rails yang membahas RSpec dan testing patterns.
Railway-Oriented Design untuk Workflow Kompleks
Railway-oriented programming memperlakukan workflow sebagai dua jalur parallel: success melanjutkan ke depan, failure keluar segera. Setiap langkah baik melanjutkan di jalur success atau beralih ke jalur failure.
# 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
endClass Result membutuhkan method then yang hanya melanjutkan pada success:
# Tambahkan ke app/services/result.rb
def then
return self if failure?
yield(value)
endJika inventory reservation gagal, payment tidak pernah berjalan. Jika payment gagal, fulfillment tidak pernah berjalan. Setiap langkah mengembalikan Result, dan chain short-circuits pada failure pertama.
Kapan Service Objects Menjadi Anti-Pattern
Service objects menyelesaikan masalah nyata tetapi dapat berkembang biak secara tidak perlu. Perhatikan warning signs berikut:
-
Single-line services: Jika service hanya memanggil satu method model, service menambah indirection tanpa value.
User.authenticate(email, password)lebih baik daripadaAuthenticateUser.new.call(email, password)ketika logic-nya sederhana. -
Services yang hanya ada untuk testing: Meng-inject dependencies membuat testing lebih mudah, tetapi membuat services hanya untuk membungkus model methods demi testability menunjukkan masalah sebenarnya adalah test setup, bukan architecture.
-
Anemic services tanpa logic: Services yang hanya mendelegasikan ke models tanpa menambah behavior hanyalah ceremony. Blog engineering Netguru membahas masalah over-extraction ini.
Alternatifnya: gunakan POROs secara selektif bersama Concerns untuk shared model behavior, Value Objects untuk domain concepts seperti Money atau DateRange, dan model methods untuk single-record operations.
Poin-Poin Penting untuk Design Rails Service Object
- Ekstrak ke service object ketika logic melintasi multiple models, memerlukan external calls, atau membutuhkan explicit error handling
- Gunakan satu method publik
callyang mengembalikan object Result, bukan raw values atau exceptions - Inject dependencies melalui constructor dengan sensible defaults untuk production use
- Beri nama services dengan verb phrases seperti
CreateAccountatauPlaceOrderyang mendeskripsikan action - Organisir services berdasarkan domain (
users/,orders/) bukan flat directory structure - Mulai dengan class Result minimal sebelum menambahkan gems seperti dry-monads
- Simpan exceptions untuk infrastructure failures, bukan business rule violations
- Hindari membuat services untuk single-line operations yang ditangani models secara natural
Mulai berlatih!
Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.
Bisakah kamu menemukan bug di Ruby on Rails?
Satu potongan kode nyata, satu bug tersembunyi, satu percobaan per hari. Tanpa akun untuk mencoba.

Ditulis oleh
Anthony Fillion-MailletPendiri SharpSkill
Developer fullstack selama lebih dari 10 tahun. Ia menjalankan SharpSkill dan bertanggung jawab atas semua yang diterbitkan di sini.
Diperbarui 20 Agustus 2026
Tag
Bagikan
Artikel terkait

Rails API Mode di 2026: RESTful API, Serialisasi JSON, dan Pertanyaan Interview
Panduan lengkap Rails 8 API Mode: rute RESTful, serialisasi Alba dan jsonapi-serializer, autentikasi JWT, error handling, dan RSpec.

Solid Queue dan Solid Cache di Rails 8: Panduan Lengkap untuk Persiapan Interview Teknis 2026
Pembahasan mendalam tentang Solid Queue dan Solid Cache sebagai komponen default berbasis database di Rails 8. Meliputi arsitektur, konfigurasi, kontrol concurrency, mekanisme caching, serta pertanyaan interview teknis yang sering diajukan di tahun 2026.

Ruby on Rails 8: Fitur Baru dan Panduan Migrasi Lengkap 2026
Rails 8 memperkenalkan Solid Trifecta, autentikasi bawaan, Kamal 2, dan Propshaft. Panduan lengkap dengan contoh kode serta langkah migrasi dari Rails 7.