All lessons

Spot the Tax · Card 20 of 20

Mutating a child without going through the parent breaks invariants

Why letting any code change a LineItem directly leaves your Order's totals out of sync.

The code

What will this cost you in six months?

class Order < ApplicationRecord
  has_many :line_items
  # total_cents is a stored column on the orders table,
  # supposed to mirror sum(line_items.price_cents * quantity)
end

class LineItem < ApplicationRecord
  belongs_to :order
end

# Anywhere in the codebase:
LineItem.find(123).update!(quantity: 0)
# The order's total_cents column is now wrong.

The problem

Anywhere in the codebase can directly change a LineItem — update its quantity, change its price, destroy it. When that happens, the order's stored total_cents doesn't get recalculated, because nobody told the order anything happened. The order's stored data no longer matches the line items it claims to have. Any rule that depends on the line items (an order has at least one item, the total is below the credit limit) gets bypassed too, because the order never got a chance to enforce them.

Take a moment. Before revealing, ask yourself: who should be allowed to change a line item? And how would you make sure the order finds out about every change?