Skip to content
Lightweight state machine for Active Record and Active Model.
Ruby Roff
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Type Name Latest commit message Commit time
Failed to load latest commit information.
.ruby-version Upgrades .ruby-version to 2.6.4 Sep 20, 2019
Gemfile.lock Bump rspec from 3.8.0 to 3.9.0 Oct 8, 2019
Rakefile Replaces rake test tasks with rspec Aug 25, 2017
logo.svg Adds logo and link to readme Sep 12, 2019
police_state.gemspec Installs activerecord-nulldb-adapter 0.4.0 Oct 2, 2019

Build Status codecov Doc Status

Police State

Lightweight state machine for Active Record and Active Model.


After experimenting with state machines in a recent project, I became interested in a workflow that felt more natural for rails. In particular, I wanted to reduce architectural overlap incurred by flow control, guard, and callback workflows.

The goal of Police State is to let you easily work with state machines based on ActiveModel::Dirty, ActiveModel::Validation, and ActiveModel::Callbacks


Police State revolves around the use of TransitionValidator and two helper methods, attribute_transitioning? and attribute_transitioned?.

To get started, just include PoliceState in your model and define a set of valid transitions:

class Model < ApplicationRecord
  include PoliceState

  enum status: {
    queued: 0,
    active: 1,
    complete: 2,
    failed: 3
  validates :status, transition: { from: nil, to: :queued }
  validates :status, transition: { from: :queued, to: :active }
  validates :status, transition: { from: :active, to: :complete }
  validates :status, transition: { from: [:queued, :active], to: :failed }

Committing a Transition

One aspect of Police State that will feel different than other ruby state machines is the idea that in-memory state has not fully transitioned until it is persisted to the database. This lets you operate within a traditional Active Record workflow:

model = :complete)
# => #<Model:0x007fa94844d088 @status=:complete>

model.status_transitioning?(from: nil)
# => true

model.status_transitioning?(to: :complete)
# => true

# => false

# => {:status=>["can't transition to complete"]}
# => false!
# => ActiveRecord::RecordInvalid: Validation failed: Status can't transition to complete

model.status = :queued
# => :queued

# => true
# => true

model.status_transitioned?(from: nil, to: :queued)
# => true

Guard Conditions

Guard conditions can be introduced for a state by adding a conditional ActiveRecord validation:

validates :another_field, :presence, if: -> { queued? }


Callbacks can be attached to specific transitions by adding a condition on attribute_transitioned?. If the callback needs to occur before persistence, attribute_transitioning? can also be used.

after_commit :notify, if: -> { status_transitioned?(to: :complete) }
after_commit :alert, if: -> { status_transitioned?(from: :active, to: :failed) }
after_commit :log, if: -> { status_transitioned? }


Explicit event languge can be added to models by wrapping update and / or update!

def run
  update(status: :active) 
def run!
  update!(status: :active)

The bang methods defined by ActiveRecord::Enum work as well:!
# => ActiveRecord::RecordInvalid: Validation failed: Status can't transition to active

Validation Logic

One important note about TransitionValidator is that it performs a unidirectional validation. For example, the following ensures that the active state can only be reached from the queued state:

validates :status, transition: { from: :queued, to: :active }

However, this does not prevent queued from transitioning to other states. Those states must be controlled by their own validators.

Active Model

If you are using Active Model, make sure your class correctly implements ActiveModel::Dirty. For an example, check out spec/test_model.rb


Add this line to your application's Gemfile:

gem 'police_state'

And then execute:

$ bundle

Or install it yourself as:

$ gem install police_state


The gem is available as open source under the terms of the MIT License.

You can’t perform that action at this time.