
đź§ Refactoring Rails: From Service Objects to Clean Maintainalbe Code
When building real-world Rails apps, it’s common to hit the limits of traditional service objects. Here’s a practical breakdown of what you need to know — and what to do instead — based on our recent conversation.
âť“ Should I use .new
or .for
for services?
Use .for(config)
when your service needs to select a subclass (e.g., inbound vs. outbound logic).
âś… Why .for
is better here:
- It acts as a factory, returning the correct subclass.
- Avoids messy branching in controllers or the initializer.
- Keeps your API consistent (
.fetch_and_persist_calls
stays the same). - Used in Rails itself (
ActiveStorage::Service.for
) and by trusted teams (like Thoughtbot).
Example:
Nlpearl::ApiService.for(config)
→ returnsInboundService
orOutboundService
based on direction.
❓ What does “Instance Methods and Associations” mean?
Instead of putting logic in a service object, you can:
- Add instance methods directly to your model
- Use ActiveRecord associations to access related data
- Encapsulate logic where it belongs (next to the data)
âś… Benefit: Calling
order.checkout
is more natural thanCheckoutService.new(order).call
.
âť“ What if my model becomes too fat?
According to this article, when a model gets too bloated:
👇 Do this instead of writing service objects:
- âś… Extract behavior into Concerns
- âś… Use POROs (Plain Old Ruby Objects)
- âś… Add meaningful instance methods
- âś… Rely on ActiveRecord associations to reduce logic complexity
- đźš« Avoid cramming everything into
.call
service objects unless absolutely needed
❓ Aren’t POROs and service objects the same?
No — but they overlap.
- PORO = Any plain Ruby class (e.g.,
Money
,Coordinate
,MessageParser
) - Service object = A PORO used for a specific process (e.g.,
CreateBooking
,SyncTickets
) - Not all POROs are service objects, but all service objects are POROs.
✅ Tip: Don’t overuse
.call
. Break logic into small POROs or model methods where possible.
âś… Summary
Situation | Best Practice |
---|---|
Selecting between inbound/outbound | Use .for(config) factory method |
Adding call logic | Use model_instance.checkout instead of service |
Fat models | Extract to Concerns or POROs |
Business process | Small, focused service object (only if needed) |
General reusable logic | Use POROs (not necessarily service objects) |