Skip to content
Separate your domain model from your persistence mechanism.
Branch: master
Clone or download
Type Name Latest commit message Commit time
Failed to load latest commit information.
spec add support for Rails 5.2 Oct 5, 2018
.gitignore Adds projectFilesBackup dir to .gitignore. Sep 7, 2014
.rspec adds colors to rspec output Jul 16, 2016
.travis.yml Use more recent ruby versions Jan 28, 2019
Appraisals Adds changelog. Sep 15, 2015
LICENSE.txt Breaks out code into own gem. Aug 28, 2014
Rakefile Allows tests to be run from the command-line and adds docs on how to … Jan 23, 2015

Vorpal Build Status Code Climate

Separate your domain model from your persistence mechanism. Some problems call for a really sharp tool.

One, two! One, two! and through and through

The vorpal blade went snicker-snack!

He left it dead, and with its head

He went galumphing back.


Vorpal is a Data Mapper-style ORM (object relational mapper) framelet that persists POROs (plain old Ruby objects) to a relational DB. It has been heavily influenced by concepts from Domain Driven Design.

We say 'framelet' because it doesn't attempt to give you all the goodies that ORMs usually provide. Instead, it layers on top of an existing ORM and allows you to take advantage of the ease of the Active Record pattern where appropriate and the power of the Data Mapper pattern when you need it.

3 things set it apart from existing main-stream Ruby ORMs (ActiveRecord, Datamapper, and Sequel):

  1. It keeps persistence concerns separate from domain logic. In other words, your domain models don't have to extend ActiveRecord::Base (or something else) in order to get saved to a DB.
  2. It works with Aggregates rather than individual objects.
  3. It plays nicely with ActiveRecord objects!

This last point is incredibly important because applications that grow organically can get very far without needing to separate persistence and domain logic. But when they do, Vorpal will play nicely with all that legacy code.

For more details on why we created Vorpal, see The Pitch.


Add this line to your application's Gemfile:

gem 'vorpal'

And then execute:

$ bundle

Or install it yourself as:

$ gem install vorpal


Start with a domain model of POROs and AR::Base objects that form an aggregate:

class Tree; end

class Branch
  include Virtus.model

  attribute :id, Integer
  attribute :length, Decimal
  attribute :diameter, Decimal
  attribute :tree, Tree

class Gardener < ActiveRecord::Base

class Tree
  include Virtus.model

  attribute :id, Integer
  attribute :name, String
  attribute :gardener, Gardener
  attribute :branches, Array[Branch]

In this aggregate, the Tree is the root and the Branches are inside the aggregate boundary. The Gardener is not technically part of the aggregate but is required for the aggregate to make sense so we say that it is on the aggregate boundary. Only objects that are inside the aggregate boundary will be saved, updated, or destroyed by Vorpal.

POROs must have setters and getters for all attributes and associations that are to be persisted. They must also provide a no argument constructor.

Along with a relational model (in PostgreSQL):

  id serial NOT NULL,
  name text,
  gardener_id integer

CREATE TABLE gardeners
  id serial NOT NULL,
  name text

  id serial NOT NULL,
  length numeric,
  diameter numeric,
  tree_id integer

Create a repository configured to persist the aggregate to the relational model:

require 'vorpal'

module TreeRepository
  extend self

  engine = Vorpal.define do
    map Tree do
      attributes :name
      belongs_to :gardener, owned: false
      has_many :branches

    map Gardener, to: Gardener

    map Branch do
      attributes :length, :diameter
      belongs_to :tree
  @mapper = engine.mapper_for(Tree)

  def find(tree_id)
    @mapper.query.where(id: tree_id).load_one

  def save(tree)

  def destroy(tree)

  def destroy_by_id(tree_id)

Here we've used the owned flag on the belongs_to from the Tree to the Gardener to show that the Gardener is on the aggregate boundary.

And use it:

# Saves/updates the given Tree as well as all Branches referenced by it,
# but not Gardeners.

# Loads the given Tree as well as all Branches and Gardeners 
# referenced by it.
small_tree = TreeRepository.find(small_tree_id)

# Destroys the given Tree as well as all Branches referenced by it,
# but not Gardeners.

# Or

API Documentation


It also does not do some things that you might expect from other ORMs:

  1. No lazy loading of associations. This might sound like a big deal, but with correctly designed aggregates it turns out not to be.
  2. No managing of transactions. It is the strong opinion of the authors that managing transactions is an application-level concern.
  3. No support for validations. Validations are not a persistence concern.
  4. No AR-style callbacks. Use Infrastructure, Application, or Domain services instead.
  5. No has-many-through associations. Use two has-many associations to a join entity instead.
  6. The id attribute is reserved for database primary keys. If you have a natural key/id on your domain model, name it something that makes sense for your domain. It is the strong opinion of the authors that using natural keys as foreign keys is a bad idea. This mixes domain and persistence concerns.


  1. Persisted entities must have getters and setters for all persisted attributes and associations. They do not need to be public.
  2. Only supports PostgreSQL.

Future Enhancements

  • Aggregate updated_at.
  • Support for other DBMSs (no MySQL support until ids can be generated without inserting into a table!)
  • Support for other ORMs.
  • Value objects.
  • Remove dependency on ActiveRecord (optimistic locking? updated_at, created_at support? Data type conversions? TimeZone support?)
  • More efficient updates (use fewer queries.)
  • Nicer DSL for specifying attributes that have different names in the domain model than in the DB.


Q. Why do I care about separating my persistence mechanism from my domain models?

A. It generally comes back to the Single Responsibility Principle. Here are some resources for the curious:

Q. How do I do more complicated queries against the DB without direct access to ActiveRecord?

A. Create a method on a Repository! They have full access to the DB/ORM so you can use Arel and go crazy or use direct SQL if you want.

For example:

  def find_all
    @mapper.query.load_all # use the mapper to load all the aggregates

Q. How do I do validations now that I don't have access to ActiveRecord anymore?

A. Depends on what kind of validations you want to do:

Q. How do I use Rails view helpers like form_for?

A. Check out ActiveModel::Model. For more complex use-cases consider using a Form Object.

Q. How do I get dirty checking?

A. Check out ActiveModel::Dirty.

Q. How do I get serialization?

A. You can use ActiveModel::Serialization or ActiveModel::Serializers but they are not heartily recommended. The former is too coupled to the model and the latter is too coupled to Rails controllers. Vorpal uses SimpleSerializer for this purpose.


  1. Fork it ( )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Setup DirEnv

Using this gem's bin stubs (contained in the bin dir) is much easier if DirEnv is installed.

On OSX using ZSH DirEnv can be installed like so:

  1. brew install direnv
  2. echo 'eval "$(direnv hook zsh)"' >> ~/.zshrc

Please see the DirEnv docs if your environment is different.

Running Tests

  1. Start a PostgreSQL server.
  2. Either:
  • Create a DB user called vorpal with password pass. OR:
  • Modify spec/helpers/db_helpers.rb.
  1. Run rake from the terminal.

Running Tests for the non-default versions of Rails

  1. Start a PostgreSQL server.
  2. Either:
  • Create a DB user called vorpal with password pass. OR:
  • Modify spec/helpers/db_helpers.rb.
  1. Run appraisal <rails version> rake from the terminal.
  • Where <rails version> is one of the options defined in the ./Appraisal file.

Please see the Appraisal gem docs for more information.


See who's contributed!

You can’t perform that action at this time.