Effective Service Objects in Ruby

matteojoliveau

Matteo Joliveau

Posted on June 4, 2018

Effective Service Objects in Ruby

As a Java developer that recently transitioned to a Ruby on Rails company, I felt kinda lost when I discovered that the use of models directly inside of controllers was a common practice.

I have always followed the good practices of Domain Driven Design and encapsulated my business logic inside special classes called service objects, so in Java (with Spring) a controller would look like this:

@Controller
@RequestMapping("/api/users")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping
    public ResponseEntity<Iterable<User>> getAllUsers() {
        return ResponseEntity.ok(userService.getUsers());
    }
}
Enter fullscreen mode Exit fullscreen mode

Verbosity aside, this is nice and clean with good separation of concerns. The actual business logic that retrieves the user list is delegated to the UserService implementation and can be swapped out at any time.

However, in Rails we would write this controller as such:

class Api::UserController < ApplicationController
    def index
        @users = User.all
        render json: @users
    end
end
Enter fullscreen mode Exit fullscreen mode

Yes, this is indeed shorter and even cleaner than the Java example, but it has a major flaw. User is an ActiveRecord model, and by doing this we are tightly coupling our controller to our persistence layer, breaking one of the key aspects of DDD. Moreover, if we wanted to add authorization checks to our requests, maybe only returning a subgroup of users based on the current user's role, we would have to refactor our controller and putting it in charge of something that is not part of the presentation logic. By using a service object, we can add more logic to it while being transparent to the rest of the world.

Let's build a service object

In Java this is simple. It's a singleton class that is injected into other classes by our IoC container (Spring DI in our example).
In Ruby, and Rails especially, this is not quite the same, since we can't really inject anything in our controller constructor. What we can do, however, is taking inspiration by another programming language: Elixir.
In Elixir, a functional language, there are no classes nor objects, only functions and structs. Functions are grouped into modules and have no side effects, a great feature to ensure immutability and stability in our code.
Since Ruby too has modules, we can use them to implement our service object as stateless collections of methods.

Our UserService can look something like this:

module UserService
    class << self
        def all_users
            User.all
        end
    end
end
Enter fullscreen mode Exit fullscreen mode

And then will be used like this:

class Api::UserController < ApplicationController
    def index
        @users = UserService.all_users
        render json: @users
    end
end
Enter fullscreen mode Exit fullscreen mode

This doesn't sound like a smart move, does it? We just moved the User.all call in another class. And that's true, but now, as our application grow we can add more logic to it without breaking other code or refactoring, as long as we keep our API stable.
One small change I'll make before proceding. Since we may want to inject some data into our service on every call, we'll define our methods with a first parameter named ctx, which will contain the current execution context. Stuff like the current user and such will be contained there.

module UserService
    class << self
        def all_users _ctx # we'll ignore it for now
            User.all
        end
    end
end
Enter fullscreen mode Exit fullscreen mode
class Api::UserController < ApplicationController
    def index
        @users = UserService.all_users { current_user: current_user }
        render json: @users
    end
end
Enter fullscreen mode Exit fullscreen mode

Applying business logic

Now let's build a more complex case, and let's use a user story to describe it first. Let's imagine we're building a ToDo app (Wow, how revolutionary!).
The story would be:

As a normal user I want to be able to see all my todos for the next month.

The RESTful HTTP call will be something like:
GET /api/todos?from=${today}&to=${today + 1 month}

Our controller will be:

class Api::TodoController < ApplicationController
    def index
        @ctx = { current_user: current_user }
        @todos = TodoService.all_todos_by_interval @ctx, permitted_params
        render json: @todos
    end

    private

    def permitted_params
        params.require(:todo).permit(:from, :to)
    end
end
Enter fullscreen mode Exit fullscreen mode

And our service:

module TodoService
    class << self
        def all_todos_by_interval ctx, params
            Todos.where(user: ctx[:current_user]).by_interval params
        end
    end
end
Enter fullscreen mode Exit fullscreen mode

As you can see we are still delegating the heavy database lifting to the model (throught the scope by_interval) but the service is actually in control of filtering only for the current user. Our controller stays skinny, our model is used only for persistence access, and our business logic doesn't leak in every corner of our source code. Yay!

Service Composition

Another very useful OOP pattern we can use to enhance our business layer is the composite pattern. With it, we can segregate common logic into dedicated, opaque services and call them from other services. For example we might want to send a notification to the user when a todo is updated (for instance because it expired). We can put the notification logic into another service and call it from the previous one.

module TodoService
    class << self
        def update_todo ctx, params
            updated_todo = Todos.find ctx[:todo_id]
            updated_todo.update! params # raise exception if unable to update
            notify_expiration ctx[:current_user], updated_todo if todo.expired?
        end

        private

        def notify_expiration user, todo # put in a private method for convenience
            NotificationService.notify_of_expiration { current_user: user }, todo
        end
    end
end
Enter fullscreen mode Exit fullscreen mode

Commands for repetitive tasks

As the Gang of Four gave us a huge amount of great OOP patterns, I'm going to borrow one last concepts from them and greatly increase our code segregation. You see, our services could act as coordinators instead of executors, delegating the actual work to other classes and only caring about calling the right ones. Those smaller, "worker-style" classes can be implemented as commands. This has the biggest advantage of enhancing composition by using smaller execution units (single commands instead of complex services) and separating concerns even more. Now services act as action coordinators, orchestrating how logic is executed, while the actual execution is run inside simple, testable and reusable components.

Side Note: I'm going to use the gem simple_command to implement the command pattern, but you are free to use anything you want

Let's refactor the update logic to use the command pattern:

class UpdateTodo
    prepend SimpleCommand

    def initialize todo_id, params
        @todo_id = todo_id
        @params = params
    end

    def call
        todo = Todos.find @todo_id

        # gather errors instead of throwing exception
        errors.add_multiple_errors todo.errors unless todo.update @params
        todo
    end
end

module TodoService
    class << self
        def update_todo ctx, params
            cmd = UpdateTodo.call ctx[:todo_id], params

            if cmd.success?
                todo = cmd.result
                notify_expiration ctx[:current_user], todo if todo.expired?
            end

            # let's return the command result so that the controller can
            # access the errors if any
            cmd
        end

        private

        def notify_expiration user, todo # put in a private method for convenience
            NotificationService.notify_of_expiration { current_user: user }, todo if todo.expired?
        end
    end
end
Enter fullscreen mode Exit fullscreen mode

Beautiful. Now every class has one job (Controllers receive requests and return responses, Commands execute small tasks and Services wire everything together), our business logic is easily testable without needing any supporting infrastructure (just mock everything. Mocks are nice.) and we have smaller and more reusable methods. We just have a slightly bigger codebase, but it's still nothing compared to a Java project and it's worth the effort on the long run.
Also, our services are no longer coupled to any Rails (or other frameworks) specific class. If for instance we wanted to change the persistence library, or migrate one business domain to an external microservice, we just have to refactor the related commands without having to touch our services.

Are you using service objects in your Ruby projects? How did you implement the pattern and what challenges did you solved that my approach does not?

💖 💪 🙅 🚩
matteojoliveau
Matteo Joliveau

Posted on June 4, 2018

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related