ActiveRecord Medium severity

Law of Demeter Violations

Chained method calls across multiple associations in views, controllers, or other models (for example order.customer.address.country.name), coupling callers to the entire object graph and crashing with NoMethodError: undefined method 'name' for nil:NilClass whenever any link in the chain is missing.

Before / After

Problematic Pattern
# In a view
<%= @order.customer.address.country.name %>
<%= @order.customer.subscription.plan.tier %>

# One nil anywhere in the chain = 500 error.
# Test setup must build the full graph every time.
Target Architecture
class Order < ApplicationRecord
belongs_to :customer
delegate :country_name, :plan_tier,
  to: :customer, allow_nil: true
end

class Customer < ApplicationRecord
belongs_to :address, optional: true
belongs_to :subscription, optional: true

delegate :name, to: :country,
  prefix: true, allow_nil: true
delegate :tier, to: :plan,
  prefix: true, allow_nil: true
end

# In the view
<%= @order.country_name %>
<%= @order.plan_tier %>

Why this hurts

Every chained traversal issues a separate lazy association load on ActiveRecord. Even with includes, the eager loader preloads the explicit association path but not its transitive hops, so order.customer.address.country materializes four separate queries if the preloader could not infer the full path. For a collection of 50 orders rendered in a table, this collapses into 200 database round-trips per page load, with latency dominated by connection overhead rather than data transfer.

Beyond the N+1 cost, long method chains introduce a systemic observability problem. Any nil in the chain raises NoMethodError at the deepest level, producing stack traces that point to the view template rather than to the missing data. Error aggregators (Sentry, Bugsnag) group these by the shallowest nil, so ten different missing-data scenarios collapse into one opaque bucket. Incident response wastes time identifying which association was actually absent. Fragment caching becomes inconsistent because cached HTML embeds the traversal result, so a stale cache on a changed country name ships wrong data until the cache TTL expires.

The object graph coupling propagates upward. Changing the schema on Country (renaming name to official_name, splitting it into short_name and long_name) forces updates across every caller in every view. Safe refactoring requires global grep, not a model-level API change, which is the opposite of object-oriented encapsulation. Tests become integration tests whether the author intended or not: rendering one cell in one view requires instantiating Order, Customer, Address, Country, Subscription, and Plan, with all their validators and callbacks running.

Get Expert Help

Inheriting a legacy Rails codebase with this problem? Request a Technical Debt Audit.