Announcing: SimpleServices

During the recent "Rails/Grails kerfluffle":http://blog.thinkrelevance.com/2008/1/11/how-to-pick-a-platform, one of the memes that came up over and over again was that Grails had a specific feature which Rails lacked and that it was a Big Deal(tm). Specifically, Grails defines a service layer with automatic transaction demarcation which allows you to remove complex domain manipulation code from your controllers, leaving them to deal with loading resources and redirecting to views.

As a thought experiment, we set out to find out what would need to happen to enable this in a Rails app. By the end, we'd written the plugin we are releasing today: SimpleServices. It amounts to about 12 lines of code, but that isn't really a boast, as we'll see in a minute.

Before we look at SimpleServices, we should discuss what makes up the service layer in Grails:

  • Services are classes which are, by default, singletons and are assumed to be stateless.
  • Their methods are wrapped in a transaction by default, though you can turn this behavior off (not sure why you would want to, but whatever)
  • Services are injected into controllers via standard Spring dependency injection methods
  • Services are singletons by default, but can be declaratively scoped to one of many other scopes: request, session, flash, etc. This would enable stateful services.
  • "Transactions", in this case, are full-fledged container-managed transactions, which can involve database transactions, messaging system transactions, and any other resources that expose a transactional API and can be enlisted in a container-managed transaction.

When we wrote this plugin, we were attempting to get out the most robust implementation of the default use case, which is:

  • singleton, stateless services
  • ActiveRecord transactions
  • encapsulation of service creation details (obscuring their instantiation API)
  • encapsulation of complex domain logic, outside of your controllers

When you install the plugin, you can create a folder in RAILS_ROOT/app called "services". Within this folder, you can define one or more service classes. A service class has a name that ends with "Service" and derives from Service::Base. The plugin causes these to be auto-loaded in the traditional Rails fashion. Each method in the service is automatically wrapped in a transaction, and any errors raised within the method will cause the transaction to rollback.

Within your controllers, you can declare which services you will be using in your actions. For each service you declare your need for, a method will be added to the controller in the form of "#{service_name}_service" which hides all the service instantiation code.

  class AccountController < ApplicationController

    services :account, :user, :security
  
    def update
      base_account = Account.find(params[:base_id])
      target_account = Account.find(params[:target_id])
      account_service.transfer(base_account, target_account, params[:amount])
    end
  end

You aren't forced to use services from controllers, either. If you want the declarative support, but in your models, you just include SimpleServices::ServiceInjection in whatever class you want. For example, you could include it in ActiveRecord::Base and have it available for all your models. Likewise, you can create instances of services directly, without declarative support, by calling AccountService.instance.

h2. Known Issues

  • The plugin has a dependency on the "Aquarium gem":http://aquarium.rubyforge.org/. In order to provide the wrapping, we needed a good aspect library. ("Aspectr":http://aspectr.sourceforge.net/ has fallen on hard times, and Aquarium seems to have a lot of activity and a very simple API.) Just install the gem before using SimpleServices.
  • We don't provide support for scoping the services yet. It seems that most people use the stateless singleton model, and this was a straightforward case to implement. We'll start looking into scoping the services next, and we'd love to hear from anybody who can make a good case for, say, a stateful, session-scoped service. I honestly can't think of a good one.
  • We are limited in this 0.1 release to single-database ActiveRecord transactions. We are looking ahead to enlisting the transaction manager when you are running in JRuby, but that will be in a future release.

We'd love to hear if people in the Rails community find this useful. We are playing around with it in a couple of apps, and it does have the benefit of cleaning up the controller codebase and providing zero-thought transaction support. Yay, services! But, we'd love to hear from others on this, and on how it could be improved.

Good luck, and happy servicing.

Get In Touch