Untangle Domain and Persistence Logic with Curator

This post is cross-posted at

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

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

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

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

note = Note.new(: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

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

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

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

<%= 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

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

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.