Service Objects no Rails em 2026: Design Patterns, PORO e Perguntas de Entrevista Técnica
Os service objects do Rails extraem a lógica de negócio de controllers e models para classes Ruby simples. Este guia cobre padrões de projeto, implementações PORO e as perguntas de entrevista técnica mais frequentes.

Os service objects do Rails extraem a lógica de negócio de controllers e models para Plain Old Ruby Objects (POROs). Esse padrão mantém as aplicações Rails manuteníveis conforme crescem, e os recrutadores o avaliam frequentemente para medir a compreensão de arquitetura limpa dos candidatos.
Um service object encapsula uma única operação de negócio. A classe tem um único método público (tipicamente call), aceita dependências através do seu construtor, e retorna um resultado previsível. Candidatos que conseguem articular por que isso importa, não apenas como implementar, se destacam.
Por Que Service Objects Resolvem os Problemas de Fat Model e Fat Controller
O Rails incentiva colocar a lógica em algum lugar. Sem orientação clara, as equipes a empurram para os models ("fat models, skinny controllers") até que as classes ActiveRecord atinjam milhares de linhas. Outros a deixam nos controllers, tornando as actions impossíveis de testar isoladamente.
Os service objects oferecem um terceiro caminho: a lógica de domínio reside em classes dedicadas que não dependem de nada do Rails exceto o que explicitamente requerem. Esse desacoplamento torna o código testável sem carregar o framework completo, portável através de diferentes pontos de entrada (controllers, jobs em background, console), e legível porque cada classe faz uma única coisa.
As Rails Guides não prescrevem service objects, mas a comunidade se padronizou em torno do padrão. A ênfase do Rails 8 na simplicidade, com Solid Queue e Solid Cache substituindo dependências externas, torna os POROs ainda mais atraentes: menos gems, mais Ruby puro.
Anatomia de um Service Object Pronto para Produção
Um service object precisa de três coisas: um construtor que recebe dependências, um único método público call, e um tipo de retorno que sinalize sucesso ou falha.
# 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
endO construtor aceita user_repo e mailer com valores padrão. O código de produção usa os defaults; os testes injetam mocks. O método call valida, persiste, envia um email, e encapsula cada resultado em um objeto Result.
Construindo uma Classe Result Mínima Sem Gems
Algumas equipes adotam dry-monads imediatamente. A gem é sólida, mas adiciona uma curva de aprendizado. Uma classe Result de 30 linhas cobre a maioria das necessidades:
# 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
endEsse Result suporta encadeamento com on_success e on_failure, expõe códigos de erro estruturados, e requer zero dependências. Os controllers o consomem de forma limpa:
# 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
endEquipes que já usam o ecossistema dry-rb, ou aquelas que precisam da notação Do para encadear múltiplas operações, se beneficiam do dry-monads. A gem fornece tipos Success, Failure e Maybe com suporte a pattern matching no Ruby 3.x.
Convenções de Nomenclatura que Sinalizam a Intenção
Duas convenções dominam:
| Estilo | Exemplo | Quando usar |
|---|---|---|
| Frase verbal | CreateAccount, SendInvoice, RefundPayment | Comandos que alteram estado |
| Substantivo com -or | AccountCreator, InvoiceSender, PaymentRefunder | Menos comum, algumas equipes preferem |
O estilo de frase verbal se lê naturalmente: Users::CreateAccount.new.call(params). O sufixo -or funciona mas adiciona sílabas sem clareza.
A estrutura de diretórios também importa. Agrupar por domínio mantém os serviços relacionados juntos:
app/services/
users/
create_account.rb
reset_password.rb
update_profile.rb
orders/
place_order.rb
cancel_order.rb
calculate_total.rb
payments/
process_payment.rb
refund_payment.rbEssa organização permite encontrar instantaneamente a lógica relacionada a um domínio. A alternativa, um diretório plano com prefixos (user_create_account.rb), se torna ingerenciável além de 20 arquivos.
Injeção de Dependências para Testes Isolados
Os construtores de service objects aceitam colaboradores com valores padrão. Essa técnica permite aos testes substituir dependências lentas ou com efeitos colaterais:
# spec/services/users/create_account_spec.rb
RSpec.describe Users::CreateAccount do
describe "#call" 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) }
context "with valid params" do
let(:user) { instance_double(User, valid?: true, save!: true) }
let(:mail) { double(deliver_later: true) }
before do
allow(fake_repo).to receive(:new).and_return(user)
allow(fake_mailer).to receive(:welcome_email).and_return(mail)
end
it "returns success with the user" do
result = service.call(email: "test@example.com")
expect(result).to be_success
expect(result.value).to eq(user)
end
it "sends the welcome email" do
service.call(email: "test@example.com")
expect(fake_mailer).to have_received(:welcome_email).with(user)
end
end
context "with invalid params" do
let(:user) { instance_double(User, valid?: false, errors: { email: ["invalid"] }) }
before { allow(fake_repo).to receive(:new).and_return(user) }
it "returns failure with validation errors" do
result = service.call(email: "invalid")
expect(result).to be_failure
expect(result.code).to eq(:validation_failed)
end
end
end
endEsses testes executam em milissegundos porque nunca instanciam models ActiveRecord reais nem enviam emails. O código de produção usa os defaults, então nenhuma configuração é necessária fora dos testes.
Compondo Múltiplos Serviços para Operações Complexas
Operações complexas encadeiam múltiplos serviços. Uma transação de e-commerce pode envolver validar inventário, processar pagamento e notificar envio. Compor esses serviços mantém cada peça testável:
# app/services/orders/checkout.rb
module Orders
class Checkout
def initialize(
inventory_checker: Inventory::CheckAvailability.new,
payment_processor: Payments::ProcessPayment.new,
notifier: Notifications::SendOrderConfirmation.new
)
@inventory_checker = inventory_checker
@payment_processor = payment_processor
@notifier = notifier
end
def call(order)
inventory_result = @inventory_checker.call(order.items)
return inventory_result if inventory_result.failure?
payment_result = @payment_processor.call(order.payment_details)
return payment_result if payment_result.failure?
order.complete!
@notifier.call(order)
Result.success(order)
end
end
endCada serviço interno gerencia sua própria lógica e retorna um Result. O serviço pai retorna antecipadamente se um passo falhar, evitando callbacks aninhados ou tratamento complexo de exceções.
Perguntas Comuns de Entrevista Técnica
Os recrutadores exploram a compreensão de service objects através de perguntas situacionais. Estes são os temas mais frequentes com respostas sólidas:
Pergunta: Quando usar um service object em vez de um método de model?
Usar um service object quando a operação envolve múltiplos models, tem efeitos colaterais (emails, APIs de terceiros), ou contém lógica de negócio que não pertence ao ciclo de vida de um único registro. Métodos de model são apropriados para queries simples ou validações específicas daquele registro.
Pergunta: Como testar service objects que interagem com APIs externas?
Injetar o cliente HTTP ou wrapper de API através do construtor. Nos testes, fornecer um double que retorna respostas predefinidas. Esse design evita chamadas de rede e permite testar caminhos de erro sem provocar falhas reais.
Pergunta: O que um service object deve retornar?
Um objeto tipado que representa sucesso ou falha. As abordagens comuns incluem uma classe Result personalizada, dry-monads, ou mesmo um struct simples com um flag booleano e um payload. A pior opção é retornar valores mistos (às vezes um model, às vezes nil, às vezes uma exceção), porque os chamadores não conseguem tratar os resultados de forma consistente.
Pergunta: Como service objects tratam transações de banco de dados?
Encapsular o bloco ActiveRecord::Base.transaction dentro do método call. Se múltiplos serviços precisam compartilhar uma transação, passar o bloco de transação como argumento ou usar um serviço orquestrador que gerencia os limites de transação:
# app/services/orders/process_with_transaction.rb
module Orders
class ProcessWithTransaction
def call(order)
ActiveRecord::Base.transaction do
Inventory::Reserve.new.call(order.items).tap { |r| raise ActiveRecord::Rollback if r.failure? }
Payments::Charge.new.call(order).tap { |r| raise ActiveRecord::Rollback if r.failure? }
order.confirm!
Result.success(order)
end
rescue ActiveRecord::Rollback
Result.failure(:transaction_failed)
end
end
endEvitando Erros Comuns
Equipes novas em service objects frequentemente cometem esses erros:
Criar service objects para tudo. Nem toda peça de lógica precisa de sua própria classe. Um callback de model simples ou um scope é às vezes a resposta correta. A sobre-engenharia fragmenta a lógica tanto que ninguém consegue seguir o fluxo.
Expor múltiplos métodos públicos. Um service object deve ter um único método público, call. Múltiplos métodos sugerem que a classe gerencia múltiplas responsabilidades e deveria ser dividida.
Ignorar o tratamento de erros. Retornar nil ou lançar exceções brutas força os chamadores a adivinhar o que deu errado. O padrão Result torna os caminhos de falha explícitos e o código chamador mais limpo.
Hardcodear dependências. Chamar User.find ou UserMailer.welcome_email diretamente dentro dos métodos previne a substituição em testes e acopla o serviço a classes concretas.
Pronto para mandar bem nas entrevistas de Ruby on Rails?
Pratique com nossos simuladores interativos, flashcards e testes tecnicos.
Organizando Service Objects em Aplicações Maiores
Conforme as aplicações crescem, as equipes precisam de convenções adicionais:
Serviços com namespace por domínio. Users::CreateAccount e Billing::CreateInvoice mantêm os contextos separados. Equipes construindo arquiteturas modulares (como Packwerk) mapeiam naturalmente os namespaces para packages.
Classes base compartilhadas com cautela. Um ApplicationService que fornece helpers de logging ou atalhos de transação pode ajudar, mas a herança se torna um fardo se a classe base crescer demais. Preferir módulos ou composição.
Consistência do padrão Result. Escolher um estilo de Result e usá-lo em todos os lugares. Misturar dry-monads, classes Result personalizadas e retornos booleanos força os chamadores a verificar múltiplos formatos.
Performance e Escalabilidade
Os service objects não adicionam overhead significativo. As chamadas de métodos Ruby são rápidas, e as alocações de objetos para classes pequenas são triviais. Os verdadeiros custos de performance vêm do que os serviços fazem, não do padrão em si.
Para operações de alta frequência, considerar:
Reutilização de instâncias de serviço. Em vez de Users::CreateAccount.new.call(params) em um loop apertado, instanciar uma vez e chamar múltiplas vezes se o serviço for stateless.
Cache de dependências. Se um serviço cria conexões custosas (pools de banco de dados, clientes HTTP), passá-las do código chamador em vez de criá-las novas em cada invocação.
Background jobs para efeitos colaterais. Mover emails, webhooks e outras operações lentas para jobs em background. O serviço se torna responsável por enfileirar o job, não por executar o efeito colateral diretamente.
Integração com Background Jobs do Rails 8
O Solid Queue do Rails 8 funciona perfeitamente com service objects. O job encapsula a chamada do serviço, mantendo a lógica de negócio separada da mecânica de enfileiramento:
# app/jobs/process_order_job.rb
class ProcessOrderJob < ApplicationJob
queue_as :default
def perform(order_id)
order = Order.find(order_id)
result = Orders::Process.new.call(order)
if result.failure?
Rails.logger.error("Order processing failed: #{result.code}")
raise result.error if result.code == :retriable_error
end
end
endA lógica de retry do job trata falhas transitórias. O service object permanece focado em processar o pedido, não em gerenciar a fila.
Conclusão
Os service objects trazem estrutura e testabilidade para aplicações Rails sem introduzir complexidade desnecessária. O padrão encapsula lógica de negócio em POROs que aceitam dependências, retornam resultados estruturados, e fazem uma única coisa bem.
Candidatos em entrevistas técnicas que conseguem explicar não apenas como implementar service objects, mas também por que o padrão melhora a manutenibilidade, demonstram a maturidade arquitetural que as equipes buscam. Os exemplos de código apresentados aqui fornecem uma base para construir e discutir service objects em produção.
A chave é a consistência: escolher convenções de nomenclatura, adotar um padrão Result, injetar dependências, e aplicar essas escolhas através da codebase. As aplicações Rails mais manuteníveis não são aquelas com as abstrações mais inteligentes, mas aquelas onde cada service object segue as mesmas regras previsíveis.
Você saberia encontrar o bug em Ruby on Rails?
Um trecho real, um bug escondido, uma tentativa por dia. Sem conta para testar.

Escrito por
Anthony Fillion-MailletFundador da SharpSkill
Desenvolvedor fullstack há mais de 10 anos. Dirige a SharpSkill e responde por tudo o que é publicado aqui.
Atualizado em 20 de agosto de 2026
Tags
Compartilhar
Artigos relacionados

Jobs em segundo plano no Rails em 2026: Sidekiq vs Good Job e perguntas de entrevista
Comparação completa entre Sidekiq, Good Job e Solid Queue para tarefas assíncronas no Rails, com as perguntas técnicas mais frequentes em entrevistas.

Perguntas de entrevista Ruby on Rails: Top 25 em 2026
As 25 perguntas de entrevista Ruby on Rails mais cobradas. Arquitetura MVC, Active Record, migrations, testing RSpec, APIs REST com respostas detalhadas e exemplos de código.

API GraphQL com Rails em 2026: graphql-ruby, Subscriptions e Perguntas de Entrevista
Construir uma API GraphQL pronta para producao com Rails 8 e graphql-ruby. Design de schema, mutations, subscriptions com ActionCable e preparacao para entrevistas.