đź§  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) → returns InboundService or OutboundService 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 than CheckoutService.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

SituationBest Practice
Selecting between inbound/outboundUse .for(config) factory method
Adding call logicUse model_instance.checkout instead of service
Fat modelsExtract to Concerns or POROs
Business processSmall, focused service object (only if needed)
General reusable logicUse POROs (not necessarily service objects)