What are Service Objects?
Well, it is a fancy term.
It means that we put code related to business rules in independent files, rather than models, controllers, or views. If you’re unfamiliar with the concept, I’d recommend reading the TopTal article on it.
Speaking of fancy terms, the most common way to write service objects is called a PORO, or Plain Ole Ruby Object. It just means you write some code without inheriting from Rails classes like ActiveRecord. It might look something like this.
attr_reader :subtotal, :order, :user, :price def initialize(user, subtotal)
@subtotal = subtotal
@user = user
end private def verify_active_user
# Check the user
# Maybe raise an error if there is a problem
end def create_order_record
# Create the order and save it to self.order
end # ...Create other private methods
For the most part, this works OK. It’s better than the old Rails mantra of “Fat Models, Skinny Controllers” since isolates your business logic. However, it gets kind of unwieldy at scale. Here’s a couple of flaws:
First, there are implicit expectations the that private methods run in sequence. Notice how the
create_order method really needs to run before the
send_confirmation_email because you would need that to know what to send an email about. However, it’s kinda hard to know that’s an exact rule. What if there were a dozen other things to do when you created an order? Would the coupling of those private methods be so obvious?
Second, how do you independently test the logic in the private methods? One could argue that you would only test this service object as a whole. However, if the service object did a dozen little interactions, would it be a good idea to test them in one giant test?
Last, it’s kind of clunky to reuse any of this logic. What if you created multiple kinds of orders? Maybe the implementation for most of the methods, i.e.
send_confirmation_email, stayed the same, but a few methods like
create_order_record were different. How do you easily reuse some of the logic in the private methods and not others?
If you’re talking at your screen right now and yelling that there are reasonable solutions to these challenges, then I’d say you’re probably right.
However, there might be an easier way! Enter the light-service gem.
The gem calls each of your service objects “organizers” and each individual step, an “action”. The data passes through the actions with a “context”, basically an object of data. So your organizer for the previous example might look like:
def self.call(user, subtotal)
with(user: user, subtotal: subtotal).reduce(
This organizer could be invoked with code like:
result = CreateOrder.call(user, subtotal)
subtotal are put on the context, then that context chains through each of those “action” classes. An individual action like say
CalculatePricemight look like:
TAX_RATE = 0.05 expects :subtotal, :user
executed do |context|
tax = context.subtotal * TAX_RATE
context.total = context.subtotal + tax
If only taxes were so simple! The action states what keys it expects on the context, which are
user. It also claims to add
total as a key onto the context. If this action did not add
total to the context, then LightService would error for you. If
user were missing from the context when the action started, it would also error. This is already much more explicit than our previous PORO service object.
Secondly, if we need to fail any action. We can call
context.fail_and_return!(“Some reason")from any part of the action. If we do, then the whole chain stops. The result of the whole service object is marked as failed. No further actions are invoked. So we don’t have to use exceptions or some other convoluted solution to track error cases.
Thirdly, testing is also just as easy. Regardless of your test suite, each action can be invoked outside of the organizer. In the case of our
CalculatesTotal action above, we can call
result = CalculatesTotal.execute(subtotal: <subtotal>, user: <user>)
total = result.total
<Assert something on total>
We don’t really care that it’s used as a part of our
CreateOrder organizer elsewhere. We can test this piece of logic independently.
Finally, we’re aren’t limited to only using our actions once. If you remember, we had a
VerifyActiveUser action. That’s something we might want to do in dozens of service objects. Well, there is no reason to rewrite it. We can simply keep reusing the same action as long as the
promises interfaces are maintained.
In the end, your application becomes comprised of dozens of service objects and hundreds of little actions, which we put in nested directories for organizational purposes. Each action is just a little function disguised as a class and service objects are just compositions of those little actions. Did I hear someone say that’s very functional?