If you have been in the Rails world for a while, you have undoubtedly heard about the discussion around whether, as a Rails app grows in size, it needs an additional "service layer" that contains all the business logic. There have been many blog posts about this topic already so I won't repeat the pros and cons here.
I for one started using services as an experiment in a couple of Rails apps I am working on/maintaining in early 2013 and must say, it has made working on these apps enormously more pleasant! Everything from reasoning about the way the apps work to finding bugs or implementing new features has become much more enjoyable!
Boil all the code!
I noticed that over time a certain amount of boilerplate code appeared that was pretty much the same in every app. So, naturally, I pulled this code out of the app and into a central place (a gem) so I could test it, fix bugs, and improve the stability and performance for all apps using it.
After more than a year, I now feel comfortable sharing this code with the world. It is still very much customized for my own apps (e.g., Sidekiq is used for background processing and Postgres as the db) but it should be fairly easy for contributors to jump in and make different parts more agnostic.
A minimal framework for writing services
So how does it work? Let's look at an example:
module Services module Users class Delete < Services::Base def call(ids_or_objects) users = find_objects(ids_or_objects) users.each do |user| user.destroy Mailer.user_deleted(user).deliver end users end end end end
This is a service that deletes one or more users. It's flexible: you can pass in a single user object, or a array of multiple user objects, or a single user ID, or - you guessed it - a array of user IDs. The only "magic" going on here is the
find_objects method but I think you can guess what it does. This method and a couple more are defined in
Services::Base from which your services should inherit.
There are a couple of basic principles of what a service should be and do in your Rails app:
- a service does one thing well (Unix philosophy)
- a service can be run synchronously (in the foreground) or asynchronously (in the background)
- a service can be unique, meaning only one instance of it should be run at a time
- a service logs all the things (start time, end time, caller, exceptions etc.)
- a service has its own exception class and all exceptions that it may raise must be of that class or a subclass
- a service can be called with one or multiple objects or one or multiple object IDs
Apart from these basic principles, you can implement the actual logic in a service any way you want.
Head over to the Github repo and have a look at the README, it describes all parts of the gem in much more detail.
Please try out the gem and let me know what you think!