For the purpose of this article, we shall consider a Rails application that includes a model for organizations and a model for users. Each organization can have many users. Furthermore, we shall limit the number of users and organization can have.

There are a handful of strategies to accomplish this, but today let’s explore how association callbacks can solve the situation described above. These callbacks hook into the life cycle of Active Record objects, allowing to work with those objects at various points. More specifically, the before_add callback can be used to ensure the number of users in an organization is bellow the limit, preventing the object from being saved to the database if not.

class User < ApplicationRecord
  belongs_to :organization
end
class Organization < ApplicationRecord
  MAX_USERS_IN_ORGANIZATION = 10

  has_many :users, before_add: :check_users_limit

  private
    def check_users_limit(_user)
      raise UserLimitExceeded if users.size >= MAX_USERS_IN_ORGANIZATION
    end
end

By causing the before_add callback to throw an exception, the user object does not get added to the collection.

class OrganizationTest < ActiveSupport::TestCase
  test 'user limits for organization' do
    org = create(:organisation)

    org.users = create_list(:user, 10)
    assert_equal 10, org.users.size

    assert_raises UserLimitExceeded do
      org.users << create(:user)
    end
  end
end

However, this approach comes with a caveat. As association callbacks are triggered by events in the life cycle of a collection, these are called only when the associated objects are added or removed through the association collection.

The following triggers the before_add callback:

irb(main):001:0> org = Organization.create(name: "Example")
=> #<Organization id: 1, name: "Example", created_at: "2019-11-17 10:33:45", updated_at: "2019-11-17 10:33:45">
irb(main):002:0> 11.times { |i| org.users << User.create(name: "User #{i}") }
Traceback (most recent call last):
        4: from (irb):3
        3: from (irb):3:in `times'
        2: from (irb):3:in `block in irb_binding'
        1: from app/models/organization.rb:13:in `check_users_limit'
UserLimitExceeded (UserLimitExceeded)
irb(main):003:0> org.users.size
=> 10

On the othet hand, the following does not trigger the before_add callback:

irb(main):004:0> User.create(name: "John Doe", organization: org)
=> #<User id: 11, organization_id: 1, name: "John Doe", email: nil, created_at: "2019-11-17 10:37:28", updated_at: "2019-11-17 10:37:28">
irb(main):005:0> org.reload
irb(main):006:0> org.users.size
=> 11