What would the ideal service layer look like? (Part 1)

W

I’m a Ruby developer and I do a lot of stuff in Rails. Rails has a lot of things ready to go out of the box, but it really lacks a service layer, and while there are a few design patterns that lend themselves to having a more positive experience with service objects, there’s nothing that is very Rails-like.

To get to this Rails-like service layer, we first need to understand what exactly the service layer is responsible for. If you were to hit rails g scaffold Post title body:textit would/should/could generate 3 service objects in services/posts: CreateService, UpdateService, DeleteService. Each one of these would accept controller params and the current_user and similar user env-specific objects in order to create, update, and delete a post, as well as perform any associated business logic for that post.

One of the many reasons to use a service object is to ensure that models don’t become littered with callbacks, and you can use the same logic in different places without having to copy-pasta code that may change over time. So what could this associated business logic look like? We could confirm that the post was successfully submitted to the user via SMS/email/push, email people who are subscribed to the post’s category that a new post was submitted, as well as index the post for searching.

So let’s code:

module Posts
  class CreateService
    def initialize(user, params = {})
      @user = user 
      @params = params 
    end
    
    def call
      create_post
      notify_user
      notify_subscribed_users
      index_for_searching
    end

    private
    
    def create_post; end
    def notify_user; end
    def notify_subscribed_users; end
    def index_for_searching; end 
  end
end

That’s a good start. But it begs the question: Do we pass the newly-created post to the other objects as an argument, or do we initialize with a nil @post and then assign it in create_post? I would argue the latter, mostly because I’m certain that the form that submitted the data would also like to know about the post object for things like validation errors and persistence.

module Posts
  class CreateService
 
    attr_reader :post 

    def initialize(user, params = {})
      @user = user 
      @params = params 
      @post = nil 
    end
    
    def call
      @post = create_post
      notify_user
      notify_subscribed_users
      index_for_searching
    end

    private
    
    def create_post; end
    def notify_user; end
    def notify_subscribed_users; end
    def index_for_searching; end 
  end
end

That looks better, but we’re no where near done: we’re just getting started.

What happens when one of those actions fail? How do we manage flow control? What about our form object and displaying errors? We’ll discuss that in part two.

By Josh