Ruby on Service Objects

Nearly three years ago I generated a new Ruby on Rails project intent on exploring some patterns off the Rails. It’s time to report back.

Problems to Solve

I’d just spent eight years at Kickstarter striving to craft a low-debt and lean monolith, but there were problems we’d never been able to solve. Our ActiveRecord callbacks and state machines were about as tightly tuned and elegant as they could be, but eventually the intrinsic complexity caught up.

  1. validations block important processes because of bugs in unrelated data
  2. varying validations by user (e.g. admins) adds a model dependency on the request context
  3. god models ascend as they harvest logic from overworked controllers
  4. moving tangled features to asynchronous workers is error-prone
  5. engineers write and share fragile support scripts to solve common issues that have not been prioritized for an admin interface
  6. adding new features requires combing through lifecycle callbacks to prevent unintended side effects
  7. making changes to lifecycle events requires surgical precision
  8. the learning curve for newcomers is high, as they work to acquire deep understanding before becoming productive

Identifying Causes

Some people may assert that the problem is inherent to monolithic architecture. After working with service objects and witnessing how they hold up when exposed to the same stress patterns, I have some confidence saying that the problem is simply overuse of ActiveRecord.

In Search of Solutions

Service objects are objects (classes) that implement a business action such as “create a user”, “save this draft”, or “publish that blog post”.

  • performing the change
  • performing or scheduling all side effects
  1. validations only block execution for relevant data issues
  2. admin validations only exist in service objects built for admins
  3. service objects extract actions from god models, leaving only computed properties
  4. service objects are already decoupled and ready to invoke from asynchronous workers
  5. when support engineers write and share service objects, they’re making productive progress towards offering a new admin action
  6. the scope of change for a new feature is much smaller thanks to encapsulation
  7. surgical precision is not required when the business logic is not intertwined
  8. newcomers have one new thing to learn (the service object pattern) but can use it to be productive much faster

Getting Started

Here’s a simple base class for getting started. It builds on a result = MyServiceObject.perform(args) interface and offers automatic transactions and a hook for instrumentation.

Tips for Success

Decide on a calling convention for your service objects. The example above looks like result = MyServiceObject.perform(arguments), using Promises for a Result interface.

Bonus

The service object pattern can be used in other frameworks and languages, which likely won’t have a library with as much investment and robust functionality as ActiveRecord. It’s a transferrable pattern that every Ruby developer should understand.

Startup engineer. Currently building @EmpaticoOrg. https://cainlevy.net

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store