Untangle Domain and Persistence Logic with Curator
This post is cross-posted at http://www.braintreepayments.com/devblog/untangle-domain-and-persistence-logic-with-curator.
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.
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
class NoteRepository include Curator::Repository indexed_fields :user_id end
find_by methods for
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.
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.