4 minute read

This post is cross-posted at http://www.braintreepayments.com/devblog/untangle-domain-and-persistence-logic-with-curator.

The problem

Ruby on Rails is a great web framework, and we use it extensively at Braintree to build web apps. One criticism of Rails, however, is that it encourages tight coupling of domain and persistence layers. The convention in Ruby on Rails is for domain objects to extend ActiveRecord::Base and directly gain persistence. This makes building small apps easy, but as applications grow, the tight coupling starts to make things more difficult.

Domain objects in more complicated systems no longer have a simple mapping into a database row. For example, money might be represented as cents and currency in the database, but as a Money object in the domain layer. Now, the domain objects have to handle both the money logic and the money persistence. Even if the code is abstracted away by a gem, it still lives in the domain object. Take a look at the methods on a brand new model in Rails:

>> class Bar < ActiveRecord::Base
>> end

>> (Bar.methods.sort - Object.methods).count
=> 391

These methods are in addition to anything you add to the model.

The solution

There’s nothing in Rails that says you have to tie your domain objects to persistence. It’s merely a convention. At Braintree, we’re using a new convention for new projects. Our domain objects do not contain any persistence logic. Instead, we’ve introduced a repository layer which is in charge of the persistence of these domain objects. This pattern is well-documented in Domain Driven Design.

Introducing a Repository layer into our applications has a number of benefits. The main benefit is that it separates out different kinds of logic. Domain logic goes into the domain objects, and can be tested in isolation without any persistence. Persistence logic goes into the repository objects, which only deal with saving and retrieving objects.

The second big benefit is that abstracting persistence logic into a repository allows us to swap persistence back-ends without much trouble. The most notable case is that we can swap in an in-memory data store for testing (or for most tests), but use Riak for the real application. Since the repository interacts with the back-end data store, the application code is the same regardless of back-end. It also allows us to use different back-ends for different kinds of data but still have a consistent pattern around persistence.

We’ve extracted this model and repository framework from our current applications and released it as curator. Curator currently supports Riak as the persistence back-end, but more back-ends are coming soon.

Domain objects include Curator::Model, which just gives helper methods like an initialize that sets instance variables. It also adds some helper methods for Rails.

class Note
  include Curator::Model
  attr_accessor :id, :title, :description, :user_id
end

note = Note.new(:title => "My Note", :description => "My description")
puts note.description

These model classes don’t have a lot of extra stuff:

>> class Bar
>>   include Curator::Model
>> end

>> Bar.methods.sort - Object.methods
=> [:_to_partial_path, :current_version, :model_name, :version]

Repositories include the Curator::Repository module:

class NoteRepository
  include Curator::Repository
  indexed_fields :user_id
end

Repositories have save, find_by_id, and find_by methods for indexed fields:

note = Note.new(:user_id => "my_user")
NoteRepository.save(note)

NoteRepository.find_by_id(note.id)
NoteRepository.find_by_user_id("my_user")

As persistence gets more complicated, repositories can implement their own serialize and deserialize methods to handle any case. For example, suppose our note object contains a PDF:

class NoteRepository
  include Curator::Repository

  def self.serialize(note)
    attributes = super(note)
    attributes[:pdf] = Base64.encode64(note.pdf) if note.pdf
    attributes
  end

  def self.deserialize(attributes)
    note = super(attributes)
    note.pdf = Base64.decode64(attributes[:pdf]) if attributes[:pdf]
    note
  end
end

As you can see, all persistence logic around PDFs lives in the Repository. The Note class does not care how PDFs are stored.

Curator in action

If you want to see a simple, fully functional Rails application using curator, check out curator_rails_example. I will detail the relevant bits below.

Thanks to Rails 3.x, you can include ActiveModel::Validations in any class to get validations. So if you want your note class to validate, it would look like:

 class Note
   include Curator::Model
   include ActiveModel::Validations
   attr_accessor :id, :title, :description
   validates :title, :presence => true
 end

You can also build forms from these objects. new.html.erb would look like:

<%= form_for @note do |f| %>                                  
  <%= f.error_messages %>                                     
                                                          
    <%= f.label :title %>                            
    <%= f.text_field :title %>                       
    <%= f.label :description %>                      
    <%= f.text_area :description, :size => "60x12" %>
                                                         
  <%= f.submit "Create" %>                                    
<% end %>                                                     

And the controller looks like:

class NotesController < ActionController::Base
  def new
    @note = Note.new
  end

  def create
    @note = Note.new(request.POST[:note])
    if @note.valid?
      NoteRepository.save(@note)
      redirect_to notes_path
    else
      render :new
    end
  end
end

That’s it. Rails makes it really easy to build forms and validate against our models.
Next Steps

Please check out the curator code and let us know what you think. Like all software, curator is a work in progress, so feel free to open issues on Github, submit pull requests, and help us make it better.

Updated: