Code Refactor: Service Pattern

1 min read

We followed the thin controller, fat model since a long long time ago. The design pattern has served us a great deal of simplicity until our model complexity is skyrocketing. Line by line, the model class. While we add more complexity(features), it became harder to maintain the code. Bukalapak code base has grown into millions of code with more than 80 engineers working on the repository, we do need a solution to keep things tidy.

We started realizing we need a change while having a lot of files that has more than 500 lines. We believe that is the right time to apply some engineering solution. While there is no scientific studies points that 500 lines is the threshold of a bad code, we hate to navigate through a file that has enormous amount of lines. 500 is our line in the sand. The second reason is that we want to avoid too many responsibilities. There is a numerous of approach to mend this and we go with service objects.

Service Design Pattern

Our implementation of service design pattern is a class that specifically designed to do certain business logic. It is an abstraction created from high level interaction. i.e. User login, pay transaction, or add product to cart.

Where to Put The Business?

The true problem that bloated a certain model is the business logic. So, the question is what do we want to do with this business logic? Since the model contains all off the business logic, we’d like to move the business logic to a another class. Concerns is an options, but putting a layer of abstraction, but still inside the model is only putting too much responsibility to a model. Fat controller makes testing very difficult while we want granular control on what module we test. In the other side of the road, service object give us the ability to scale huge amount of model logic and ease our testing.

Implementation

We have some common grounds while implementing service object:

1. each service must be a class with one specific purpose and one public method.
2. a particular service could be a wrapper for another service.

This is our example for service class.

module UserService
  class Login < Services::Base

    def initialize(user_session)
      @session_params = user_session
    end

    def perform
      user_session = UserSession.new(@session_params)
      if user_session.save
        # Simplification of user login logic
        return user_session.login
      end
      
      # Standard logging
      Rails.logger.info(message: "User succesfully login #{ user_session.user_id }")
      
      # Any other logging or after logging callback

      # Standard return
      return user_session
    end
  end
end

Since one class only serve one public method, a single purpose, theoretically, we can scale to huge amount of business logic without worrying a certain class became bloated. In order to stay relevant, we need to do a lot of refactoring. It costs us some stability issues, but we do dare to occasionally break and change how things are to grow. Stay tuned for more stories about our experience on scaling Bukalapak.

How I Design My Code

P.S: This post is also available at Medium Programming is becoming mainstream nowadays. I saw people code and build some applications. As a programmer,...
Indra Saputra
1 min read

Leave a Reply

Your email address will not be published. Required fields are marked *