All lessons

Spot the Tax · Card 16 of 20

Models that don't know their own rules

Why an Order model that's just data bag ends up with the same business rules duplicated in every caller.

The code

What will this cost you in six months?

class Order < ApplicationRecord
  belongs_to :user
  has_many :line_items
end

class CheckoutController < ApplicationController
  def update
    order = Order.find(params[:id])

    if order.line_items.empty?
      render json: { error: "Order is empty" }, status: 422
      return
    end
    if order.user.suspended?
      render json: { error: "Account suspended" }, status: 403
      return
    end
    if order.total_cents > order.user.credit_limit_cents
      render json: { error: "Over limit" }, status: 422
      return
    end

    order.update!(status: "submitted", submitted_at: Time.current)
  end
end

The problem

The Order model is just an object that holds data. All the rules about "can this order be submitted" live in the controller. The admin panel that lets support submit orders on behalf of customers has its own copy of those rules. So does the API. So does the rake task that recovers stuck orders. Forget one rule in one place and that path will let through orders that violate the business invariants.

Take a moment. Before revealing, ask yourself where the rules really belong. If you wanted to make the rules impossible to forget, where would they live?