Skip to main content

ActiveRecord anti-patterns which you should avoid. Part 1: Conditional validations

Updated 2 min readMarkdown

Imagine that you develop an e-commerce platform. Your basic model is Order.

You would have some constraints which don't have to be valid before order is placed:

validates :order_items, length: {minimum: 1}, unless: :cart?
validates_presence_of :delivery_address, :delivery_type, :payment_type, unless: :cart?

You would alse need some validations which have to be checked on cart submition:

validates_inclusion_of :delivery_type, in: :available_delivery_types, on: :being_placed_via_cart
validates_inclusion_of :payment_type, in: :available_payment_types, on: :being_placed_via_cart
validates_acceptance_of :shop_rules_acceptance, :terms_of_anti_spam_policy_acceptance, on: :being_placed_via_cart

... but not always:

validates :electronic_order_rules_acceptance, acceptance: true, if: :electronic_order?, on: :being_placed_via_cart

Others should be checked only if the order is passed through an external system.

validates_presence_of :external_id, on: :being_placed_via_api

All the above leads to situations in which you get confused which validations would be triggered on certain save.

So... What to do instead?

I suggest using form/command objects.

class PlaceOrderViaExternalApi < PlaceOrder
  validates_presence_of :external_id
end

class PlaceOrder < Command
  #...
end

class Command
  include ActiveModel::Model
  include ActiveModel::Attributes
  include ActiveModel::Validations
end

PlaceOrderViaExternalApi.new(order_params).valid?

You can initialize and validate them just like ActiveRecord objects but your model's code keeps cleaner and more maintainable.

No comments yet. Be the first to comment.

More from this blog