ActiveRecord Medium severity

Missing inverse_of

Bidirectional ActiveRecord associations declared without the inverse_of option, causing Rails to instantiate two separate Ruby objects for what is the same database row, producing stale state and memory duplication during in-memory graph traversals.

Before / After

Problematic Pattern
class Post < ApplicationRecord
has_many :comments
end

class Comment < ApplicationRecord
belongs_to :post
end

# In code
post = Post.find(1)
comment = post.comments.first
comment.post.equal?(post) # => false
# Two distinct Ruby objects, same DB row.
# Mutations on one are invisible to the other.
Target Architecture
class Post < ApplicationRecord
has_many :comments, inverse_of: :post
end

class Comment < ApplicationRecord
belongs_to :post, inverse_of: :comments
end

# In code
post = Post.find(1)
comment = post.comments.first
comment.post.equal?(post) # => true
# One object. Mutation on either side is visible.

Why this hurts

Without inverse_of, ActiveRecord treats the two directions of a relationship as independent, materializing a fresh object from the database for each traversal even when the data is already loaded in memory. A loop over 1,000 comments from post.comments allocates 1,000 Post instances (one per comment.post call), each with its full attribute hash, association cache, and dirty tracking overhead. Ruby’s garbage collector now has 1,000 objects to track per page load that should have been one, doubling GC pressure and multiplying major collection frequency.

The staleness problem is subtler and more dangerous. Validations that compare child records to the parent see a different in-memory instance, so changes made to the parent within the same transaction are invisible to the child. A validation like validates :body, presence: true, unless: -> { post.draft? } queries a stale post.draft? that reflects the state before the current transaction modified it. Race conditions that would be trivially safe with object identity become real bugs because the model layer cannot trust its own object graph.

Rails 6.1+ auto-infers the inverse for straightforward has_many / belongs_to pairs, but the auto-inference breaks for has_many :through, has_and_belongs_to_many, associations with custom foreign_key or class_name, scoped associations, and polymorphic relationships. Every codebase grown over multiple Rails versions accumulates these edge cases silently. bullet and rails-mini-profiler do not detect the problem because no extra queries fire, only extra allocations. Memory profilers show duplicate objects with identical attributes but different object_id, which is the diagnostic signature.

Get Expert Help

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