Extracting Service Objects From Workers

Posted on by in Web

Background jobs processed by asynchronous workers, are a staple of most production Rails applications I work with. Inevitably there is some time consuming process that you want to handle in the background, either because you want to keep user requests snappy or because you need to process things outside of the context of a user’s request. Unfortunately, these worker classes frequently become a rat’s nest of business logic and responsibilities and rarely do they get refactored. In this post, I’m going to give an example of one such worker and explain how to refactor your code to be better organized, more easily read, and better tested. Usually a worker starts out fairly simple.

In this example, we have a comment thread system. When a user receives a reply from another user, we want to notify them via email. The controller figures out who is being replied to and tells the worker to asynchronously send a notification email to them. It rarely remains this simple though. Eventually we decide there’s some other things we want to do when a comment is created, like notifying all the other participants in the conversation that there’s been a new comment, and now our worker looks like this:
There’s several things wrong with this:

  1. We have a bunch of important business logic packed into a single method.
  2. The worker has several responsibilities: deserializing the job from the job queue, notifying other thread participants and knowing how to notify them, notifying the replied-to person and knowing how to notify them, and telling Kissmetrics that a comment was created.
  3. The CommentCreatedWorker object knows too much about the implementation details of CommentThread and Comment.
  4. This is difficult to test. Let’s start refactoring this by creating a service object called CommentCreator.


I’ve done a couple of things here. I’ve taken all of the business logic out of the worker and moved it into our new service object. The worker now has a single responsibility: deserializing the CommentCreatedWorker job from the queue and telling the CommentCreator service object to finish processing. I’ve also given the CommentCreator the additional responsibility of persisting the comment to the database. Now all logic related to what happens when a new comment is created is located in the CommentCreator object We’ve simplified the worker, but now the service object now has too many responsibilities. That raises the question of, “what should a service object be responsible for?” If you’re not familiar with the concept of a service object, think of it like a symphony conductor. A conductor is responsible for making sure all the instruments play their parts at the right time, but a conductor is NOT responsible for knowing how to play all the instruments. Similarly, a service object acts as a coordinator for a single complex interaction in your system, but it is not responsible for knowing how all the individual pieces of that interaction work. In this example, our complex interaction — our symphony — is the creation of a comment. Our conductor is our CommentCreator service object. The pieces of our interaction — our instruments — are the 4 steps that have to be completed when creating a comment:

  1. persisting the comment to the database
  2. telling Kissmetrics that a new comment was created
  3. notifying the comment thread’s other participants that a new comment has been made
  4. notifying the person who’s being replied to that they received a reply

Two of our “instruments” are already their own objects: #1 and #2. The Comment object takes care of persisting the comment to the database (#1) and the Kissmetrics object takes care of reporting the event to Kissmetrics (#2). But #3 and #4 still need to be separated out into their own objects — their own instruments. Let’s start by extracting #3: the notification of the comment thread’s other participants.
Now let’s extract #4: notifying the person who’s being replied to that they received a reply.


Great! Now we have our 4 “instruments”: Comment, ThreadParticipantsNotifier, CommentReplyNotifier, and Kissmetrics, and we have our conductor: CommentCreator. Let’s put it all together and make our symphony:
There are some major wins with this refactor:

  • We’ve achieved a clear separation of concerns.
  • If we need to add another step to the creation process, it’s very clear that we’re going to add another line to our CommentCreator#perform method. If notifying a user becomes more complex than sending them an email — say we want to add in-app notifications — we’re simply going to add an additional line to our ThreadParticipantsNotifier#notify_participant method.
  • It’s much easier to test these objects in isolation than it was to test our old worker. We can mock out each object’s dependencies and test them with fast unit specs rather than relying on slow-running feature specs.
  • It’s just easier to read and comprehend what’s going on.

Feedback

  Comments: 12

  1. Paweł Gościcki


    You should not be afraid to call your service object classes with verbs, instead of nouns.

    `CreateComment#call` is really ok!


    • In fact, I’ve found it’s usually better.
      (1) you can stub this out for a test very easily
      (2) you never have to think about the name
      (3) it makes it clearer that these objects are really just functions


  2. Great article, I would love to see if you don’t mind to show on how do you test all of these classes together from controller to service classes.

  3. Augie De Blieck Jr.


    Thanks for the conductor analogy. It helped me to understand the example a lot. I’ve heard the terms and kinda understood them, but it’s nice to have it spelled out. (And, not to be the pedant, but you have a typo with “partipiant” twice in the examples…)

  4. Stanislav Spiridonov


    CommentCreator#create! doesn’t seem to be returning Comment object, but you still assign the result of this function to @comment variable.


  5. I wonder why you create twice the conductor CommentCreator?
    Would it not be sufficient if create! is a class method and has one parameter, the comment? Than it could return the comment so that you can assign it to @comment.


    • It’s initialized twice because part of the creation process happens asynchronously and the worker process needs to initialize the object later to handle the email notifications and Kissmetrics reporting. If you wanted, you could make both create! and perform! class methods whose argument is the comment. That’s a matter of style.

      One thing I wouldn’t do is create an uninitializable object. I’ve noticed many of my fellow Rubyists seem resistant to making objects that represent actions or verbs. Instead, they tend to create a module or a class they never actually initialize with a bunch of class methods. That I wouldn’t do.

  6. Brandon Hilkert


    I often struggle with the method names of classes like this. Curious if you kept the `perform` method name because it gave an indication that it was the part doing the work for the Sidekiq worker?

    To me, names like that feel so generic, including `call` and `execute`. But I struggle to think of anything better…


    • Yea, I struggle with that as well. It does feel very generic, though I’m not sure it would be appropriate for it not to be generic. If I called it “notify,” then the Kissmetrics part of the method wouldn’t make sense and as the quantity and variety of things executed by “perform” increased, it would be less and less descriptive of what was actually going on. That method is essentially “finish the job and do everything that I don’t want to do synchronously with the client request.” I suppose if you called it “finish” or “complete” it would be a little more descriptive than “perform,” but no less generic. This may be one of those rare cases where genericness is appropriate.


  7. Neat write up Marc! Service objects in Rails can definitely help in designing clean and maintainable code. Here’s an example: https://netguru.co/blog/service-objects-in-rails-will-help

Your feedback