class Order < ApplicationRecord
class InvalidTransition < StandardError; end
enum status: { pending: 0, paid: 1, shipped: 2, cancelled: 3 }
has_many :order_transitions, -> { order(:created_at) }, dependent: :destroy
TRANSITIONS = {
"pending" => %w[paid cancelled],
"paid" => %w[shipped cancelled],
"shipped" => %w[],
"cancelled" => %w[]
}.freeze
def transition_to!(target, actor:, reason: nil)
target = target.to_s
transaction do
lock!
unless TRANSITIONS.fetch(status, []).include?(target)
raise InvalidTransition, "cannot move order ##{id} from #{status} to #{target}"
end
previous = status
update!(status: target)
order_transitions.create!(
from_status: previous,
to_status: target,
actor: actor,
reason: reason
)
end
self
end
def pay!(actor:)
transition_to!(:paid, actor: actor)
end
def ship!(actor:)
transition_to!(:shipped, actor: actor)
end
def cancel!(actor:, reason: nil)
transition_to!(:cancelled, actor: actor, reason: reason)
end
end
class OrderTransition < ApplicationRecord
belongs_to :order
validates :from_status, :to_status, presence: true
validates :actor, presence: true
after_initialize do
@persisted_snapshot = persisted?
end
# Append-only: once written, a transition row is never mutated.
def readonly?
@persisted_snapshot == true
end
def to_s
"#{from_status} -> #{to_status} by #{actor}"
end
end
class OrdersController < ApplicationController
before_action :set_order, only: %i[cancel]
def cancel
@order.cancel!(actor: current_user.email, reason: params[:reason])
render json: { id: @order.id, status: @order.status }, status: :ok
rescue Order::InvalidTransition => e
render json: { error: e.message }, status: :unprocessable_entity
end
private
def set_order
@order = Order.find(params[:id])
end
end
This snippet models an order lifecycle as an explicit finite state machine backed by ActiveRecord, avoiding the temptation to scatter status checks across controllers. The core idea is that only a small, declared set of transitions is legal, each transition is guarded, and every successful move is recorded as an immutable audit row so the history of an order is reconstructable.
In Order model, status is a Rails enum mapping symbolic states to integer columns, which keeps the storage compact while giving scopes and predicate methods for free. The TRANSITIONS constant is the single source of truth: it maps each state to the states reachable from it. The transition_to! method is the only sanctioned way to change status. It rejects illegal moves with InvalidTransition, wraps the change and the audit write in a single transaction so they succeed or fail together, and uses lock! to take a row-level SELECT ... FOR UPDATE. That pessimistic lock is what makes concurrent transition attempts safe: two workers trying to pay! the same pending order will serialize, and the loser re-reads a state that no longer allows the move.
The convenience methods pay!, ship!, and cancel! are thin wrappers that name the business intent while delegating the actual guarding to transition_to!. Because the guard lives in one place, adding a new state means editing TRANSITIONS rather than hunting for conditionals.
In OrderTransition model, each row captures from_status, to_status, an actor, and a free-form reason. Marking it effectively append-only (readonly? returns true after creation, and there is no update path) turns the table into a trustworthy audit log; nothing rewrites history. The belongs_to :order plus the ordered has_many on the order side let the timeline be queried directly.
In OrdersController#cancel, the HTTP layer stays thin: it loads the order, calls the domain method, and translates the Order::InvalidTransition exception into a 422 rather than leaking a 500. This separation means the controller never decides what a legal cancellation is; it only reports the outcome. The pattern is worth reaching for whenever an entity has meaningful lifecycle rules, since it centralizes legality, gives concurrency safety, and produces an audit trail almost for free. The main trade-off is the pessimistic lock's contention cost, acceptable here because transitions are infrequent relative to reads.
Related snips
require 'application_system_test_case'
class TasksTest < ApplicationSystemTestCase
test 'deleting a task removes it from the list' do
task = tasks(:one)
System test: asserting Turbo Stream responses
class Post < ApplicationRecord
belongs_to :author, class_name: 'User'
has_many :comments, dependent: :destroy
scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
scope :draft, -> { where(published_at: nil) }
ActiveRecord scopes for reusable query logic
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
class PostsController < ApplicationController
def index
@posts = Post.includes(:author)
.order(created_at: :desc)
.page(params[:page])
.per(10)
Turbo Frames: infinite scroll with lazy-loading frame
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
Share this code
Here's the card — post it anywhere.