Back to Course

Interview Question

"Tell me about service objects"

Every Rails team has an opinion on service objects. What they solve, the two-schools framing seniors are expected to hold, the shape that works, and the smells that signal over-use.

What the interviewer is actually checking

Service objects are a contested pattern. The DHH and 37signals camp argues they are usually unnecessary and a sign that the developer has not learned to make Active Record models work harder. The "Rails at scale" camp (Shopify, GitLab, Discourse) argues they are essential for keeping large applications navigable. Both camps ship production Rails apps successfully.

The interviewer is testing whether the candidate can hold both views, explain when each is right, and avoid sounding like they read one blog post. A flat "I always use service objects" or "service objects are bad" answer signals limited exposure. The senior answer engages with the trade-off.

Behind the question is also a check on naming, code organization, and whether the candidate has internalized SRP beyond the textbook level. The full deep-dive is in Service Objects in Real Rails Code.

The mid-level answer

Typical mid-level:

"Service objects are plain Ruby classes for complex business logic. You put them in app/services/ and call them with SomeService.call(...). They are good because they keep controllers thin and keep business logic out of models. I use them when an action is too complex for a controller."

This is the textbook answer. It is not wrong, but it sounds memorised. Three problems with stopping here: it does not acknowledge the controversy, it does not say what "too complex" means, and it does not address what makes a service object good versus bad.

For a senior role, this answer signals "uses the pattern reflexively without thinking about whether it fits."

The senior answer

Four points: what the pattern is for, the trade-off, the shape that works, and the failure modes.

What it is for. "A service object isolates a domain operation that spans multiple models, has multiple steps, or needs to compose with other operations. Charging a customer's card is a service: it touches Payment, Order, User, an external API, and a notification job. It does not naturally live on one model. Naming it ChargeCustomer and putting it in app/services/ gives it a place and a clear interface."

The trade-off. "DHH's argument against service objects is that they often duplicate what well-designed models could do. If CreateUser wraps User.create! with a callback, the service is mostly noise; the model could own it. The argument for service objects is that real domain operations often span multiple models in ways that do not fit cleanly into a single model's responsibility, and that scoping them as their own object keeps the codebase navigable as it grows. Both arguments have merit. The right answer for a specific operation depends on whether it lives within one model's natural scope or crosses many."

The shape. "A working service object has a clear, single-method public interface (call or a verb-named method), explicit inputs as constructor args or method args, returns a value or raises (not 'returns success/failure as a struct' as the only mechanism), and is testable without mocking the world. The 37signals counter-pattern is to put the same operation as an instance method on the model that 'owns' the operation, which is fine when the operation does own to one model. Both are valid; the smell is when service objects multiply without a clear domain story behind them."

The failure modes. "Three common smells. First, services that wrap one model method with no added value. UpdateUser.call(user, attrs) that only calls user.update(attrs) adds friction without buying composability. Second, services that take ten optional arguments because they grew over time. Third, services that compose with each other in ways nobody documented, creating a hidden call graph. Each of these signals the pattern has been adopted as identity rather than as a design choice."

The follow-ups

"What naming convention do you use?" The right answer names a specific convention and the reason. Most teams pick verb-named classes (ChargeCustomer, SendInvoice) with a single public method (call or the verb itself). Some teams use suffixed names (CustomerCharger); both work. The bad answer is "we use whatever name feels right" because it signals no convention.

"How do you handle failure?" Three valid patterns. Raising domain-specific exceptions and letting callers rescue. Returning a result object (a struct with success/failure plus payload). Using a gem like dry-monads for Result types. Each has costs: exceptions are simple but couple control flow; result objects are explicit but verbose; dry-monads is rigorous but learning-curve heavy. The senior answer picks one and explains the trade-off, not the pattern.

"When would you NOT use a service object?" This is the key follow-up. The right answer: when the operation is small and lives within one model's responsibility, when an Active Record callback or method captures it cleanly, when the team's existing convention is to use the model and adding a service is friction. The pattern is one of many; the senior call is when to reach for it and when not.

"How do you test them?" The right answer: like ordinary Ruby classes. Stub external collaborators at the boundary (HTTP clients, payment APIs); do not mock the world. The service should be testable with three or four fakes injected via the constructor. If a service requires 15 mocks to test, it is too coupled.

The two-schools framing

Senior Rails developers in 2026 are expected to be aware that the community splits on this question.

The 37signals school: fat models, controllers that route, very few service objects. Operations that span models go on the parent model or in concerns. The codebase stays small because the team is small and the domain is well-known.

The Rails-at-scale school: thin models, thin controllers, service objects for any operation that spans models or has multiple steps. The codebase stays navigable because new engineers can read app/services/ and see all the domain operations as named units.

Both ship great Rails apps. The pattern that fails is mixing schools without intention: half the operations on models, half in services, no rule about which goes where. The senior answer acknowledges the split and names which school the candidate's previous team used and why.

What signals what

  • "I always use service objects for any complex operation." Mid. Pattern as identity.
  • "DHH says service objects are unnecessary; I agree." Mid in a different direction. Borrowed opinion.
  • "On my last team we used them for cross-model operations. Single-model ops stayed on the model." Senior. Has a rule and applied it consistently.
  • "We tried service objects in year one, ended up with 200 of them and a hidden call graph, then refactored to a smaller set named by domain operations rather than CRUD actions." Senior+. Has lived through the pattern's failure mode and learned.

The signal is whether the candidate treats the pattern as a tool with trade-offs or as a rule that defines good Rails code.

The principle at play

The right answer to most Rails design questions is "it depends, and here is the trade-off." Service objects are a clean example. The candidates who do best on this question are the ones who have shipped Rails apps in both schools and can name the smells of each. The candidates who do worst are the ones who picked a side reading a blog post and never tested the position against contrary evidence.

The interview reframing: when you get this question, lead with "what is the operation actually like?" Some operations want to be service objects. Some want to be model methods. The skill is telling them apart and committing to a convention either way.

Related lessons