Introducing Mutations: Putting SOA on Rails for security and maintainability

I’d like to present the marrying of a few techniques that have transformed the way we write Rails apps at UserVoice, and the gem we extracted from it. But first, let me explain why we started looking into techniques that veered off the Omakase version of Rails.

Problems

We had three specific problems at UserVoice:

  1. We have different user interfaces to UserVoice: the front-end where folks come to look at ideas and vote, the admin console where admins moderate their ideas and respond to tickets, the API, widgets, a Facebook app, the iOS SDK, etc. All of these interfaces have their own controllers. We have the potential for duplicate logic: there could be an ideas#create endpoint in multiple controllers.

  2. In the API especially, folks would pass in all kinds of invalid input. For instance, if an ideas#create endpoint took params[:idea][:name], they’d just pass params[:idea] as the string "Here's my idea". This would pollute our exception logs.

  3. Some of our models became extremely fat, with a sequence of lifecycle callbacks that feed into each other. It became hard to reason about and modify these models, especially when you’re dealing with logic that has to work in multiple contexts (create vs update vs business action A vs business action B).

Solution

We’ve done three things to solve this.

First, as is more and more common in the Prime Stack, we’re been using the Service Layer pattern more. Each service object is essentially a module of code that has one primary method and one responsibility. For instance: TwitterPoster.post(tweet, user).

We’ve called these service objects ‘Mutations’, as quite often they represent a change of state in our database from state A to state B.

Second, we took a hard look at input validation and made some big changes. In particular, in Rails apps you’re presented with a params hash from the user, and you’re supposed to write a function against it. Rails validates the params hash in a few ways:

  1. ActionPack forces the hash to have a general structure by construction: the hash only has strings as keys, and values can be strings, more hashes, arrays, and files.
  2. attr_accessible or strong_attributes does attribute whitelisting.
  3. ActiveModel does field validation.

This trio is usually pretty effective. However, there are some problems:

  1. The structure of the hash is frequently assumed to be correct. For instance, it’s taken for granted that params[:user] is a hash, and params[:user][:role_id] is a string (and not, say, an array)
  2. attr_accessible suffers from context blindness: you’re frequently going to have an end user UI and an admin UI. You want admins to have access to more fields.
  3. It works best if you are mapping inputs directly to database fields.

Lastly, on our more beastly AR models, we started removing lifecycle callbacks and putting them into the service objects. This makes the AR models quite anorexic. The focus is on context: each Mutation operates in a single context, so there’s very little conditional logic. Each Mutation is a clear, linear sequence of code.

The Mutant Baby

And so were born Mutations! Simply, it lets you validate and specify inputs on your service objects.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class UserSignup < Mutations::Command

  # These inputs are required
  required do
    string :email, matches: EMAIL_REGEX
    string :name
  end

  # These inputs are optional
  optional do
    boolean :newsletter_subscribe
  end

  # The execute method is called only if the inputs validate. It does your business action.
  def execute
    user = User.create!(inputs)
    NewsletterSubscriptions.create(email: email, user_id: user.id) if newsletter_subscribe
    UserMailer.async(:deliver_welcome, user.id)
    user
  end
end

and is callable like this:

1
2
outcome = UserSignup.run(params) # option A - get back an outcome that has outcome.success?, outcome.errors, and outcome.result
user = UserSignup.run!(params)   # option B - return the result, and raise an exception if there's a problem.

First, you specify the required and the optional inputs to the “function”. You also specify their type and constraints. If the inputs don’t match the constraints, and can’t be coerced to them, then the inputs are invalid and execute won’t be called. If everything checks out, then the execute method is called. Within execute, all of the inputs are safe, whitelisted values. You know all of the required values are present, and they’re the right types. This makes it a lot easier to write correct code against these inputs.

This looks pretty simple but it turns out to be pretty useful. There are a lot of synergies that arise and writing your application like this has a variety of benefits.

For complete documentation, see the README.

Reduce Code Duplication

By building an internal API (service layer) that your controllers can use, you’ll have less code duplication if you have multiple controllers that operate on the same types of data. This is straightforward and can be achieved by building any service layer, as I imagine many other folks using the Prime Stack have found.

Security & Correctness

One of the core benefits is security. There have been several Rails exploits in the past year (CVE-2013-0155 and CVE-2012-5664) that are related to an ‘incorrect datatype’ – you expected params[:id] to be a string, but in fact it was an array. These are cases where there was a bug in the code due to lack of robustness, and causing that bug also happened to reveal a security issue. There are plenty of other cases where bugs of the same type – “type checking bugs” – are simply non-exploitable bugs.

Using Mutations decreases the likelihood of issues like this popping up. If you specify that a parameter must be a string, then inside the execute method, it is guaranteed to be a string. If an ignorant or malicious user passes in an array, then an error will be raised. If the parameter can be safely coerced to a string (eg, an Integer or boolean is passed; 3 becomes “3” and false becomes “false”), then the coercion is done and you have string. This makes it much easier to write correct code, and correct code is much more likely to be secure.

Reasoning About Logic

The benefit I’m most excited about is that it’s really easy to reason about a Mutation, because it only does one thing. In contrast, it’s more difficult to reason about callback soup.

With a Mutation, you have a single business rule that you’re coding against. You’re likely to have a short, linear method.

With fat models, first of all you don’t know what the possible business operations are. But let’s say you’re thinking about creation. You have to look at the callbacks, and figure out which ones will be called in this context. In some cases, you have to look at model instance variables, which encode which context you’re in. Sometimes, information is passed from one callback to another callback. And when you’re writing callbacks, you have to make sure your piece of code works correctly in all contexts.

Callbacks are great for small apps and for simple things that are context-insensitive. For moderately complicated models, callbacks are a terrible way to encode your business rules.

Documentation

How often have you seen methods with signatures like this?

1
def do_task(user, idea, options = {}); end

user and idea are clear, but what are valid options? If you’re lucky, do_task is a short function and you can see which keys and values can be in options. If you’re not, the method is implemented like this:

1
2
3
def do_task(user, idea, options = {})
  user.foo(task_helper(idea, options))
end

And now you’re on a wild goose chase. The standard solution to this is a comment above the method. But as dogma goes, code shouldn’t need comments because code should be clear already.

Mutations solves this fairly nicely: all “options” are documented as code, not as comments. It’s extremely clear what options can be, including their types and constraints. Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
class MyTask < Mutations::Command
  required do
    model :user
    model :idea
  end

  optional do
    string :name_override
    integer :count, default: 1
  end

  def execute; ...; end
end

Notice that you know what is required, what is optional, what default values are, and what the data types of these fields are. This is more verbose than the original method, of course, but could be on-par with the original method plus a comment block.

Forms

Mutations help with form development in three ways:

  1. Multi-model forms are handled well. The standard solution is to use accepts_nested_attributes_for. Sometimes this works great, but other times you find yourself bending over backwards to do validations and other logic with callbacks. With Mutations, the structure of a nested hash can be well specified, and it’s easy to write logic against that instead of handing it over directly to ActiveRecord.

  2. Forms where the input tags don’t map exactly to database fields are handled nicely. The standard solution is to add an attr_accessor to the model, whitelist it, and then operate on it in callbacks. This sucks. It’s much nicer to specify this parameter as an input and operate on it in a simple method.

  3. Dual client-side/server-side errors are painless with Mutations. When you run a Mutation against a user’s profile, for instance, this is what you can get back:

1
2
3
4
5
6
outcome = UpdateProfile.run(params)
unless outcome.success?
  outcome.errors.symbolic # => {name: :required, address: {street: :invalid, state: :not_state}}
  outcome.errors.message # => {name: "Name is required", address: {street: "Street is invalid", state: "That's not a known state"}}
  outcome.errors.message_list # => ["Name is required", ...]
end

Conclusion

Mutations are great if you’re working on a bigger, long-lived application in a team setting. The service layer aspect gives you a scalable architecture. The input validation aspect makes it easy to pass user input directly into your service layer. You’ll have more robust and secure code.

To learn more, take a look at the Mutations home page.

Comments on Hacker News