# Performance Optimization and N+1 Query Prevention in PropertyWebBuilder

> Optimize PropertyWebBuilder performance and prevent N+1 queries using eager-loading scopes, controller includes, and Bullet gem detection.

- Repository: [Ed Tee/property_web_builder](https://github.com/etewiah/property_web_builder)
- Tags: performance
- Published: 2026-03-01

---

**PropertyWebBuilder eliminates N+1 queries by combining centralized eager-loading scopes in models, consistent `includes` chains in controllers, and the Bullet gem for runtime detection.**

PropertyWebBuilder is a multi-tenant Rails application managing large datasets of properties, listings, and users. To maintain fast response times and reasonable database load, the codebase implements a **pro-active eager-loading strategy** that prevents N+1 queries at the architectural level. This guide examines the specific patterns implemented in the `etewiah/property_web_builder` repository to optimize database performance.

## Centralized Eager-Loading Scopes in Models

The application centralizes reusable eager-loading logic within model concerns to ensure consistency across controllers. The primary implementation lives in [`app/models/concerns/listed_property/searchable.rb`](https://github.com/etewiah/property_web_builder/blob/main/app/models/concerns/listed_property/searchable.rb), which provides scopes that bundle required `includes` calls.

The `with_eager_loading` scope pre-loads the associations most commonly accessed in property listings:

```ruby

# app/models/concerns/listed_property/searchable.rb

scope :with_eager_loading, -> {
  includes(:website, :sale_listing, :rental_listing,
           prop_photos: { image_attachment: :blob })
}

```

Using `includes` allows ActiveRecord to load the associated records in a single SQL query (or two for `has_many :through` relationships), preventing individual queries when the view accesses area, price, or photo data. For lighter-weight widgets that require only photo references without the heavy ActiveStorage blob data, the concern also provides a `with_photos_only` variant.

### Extending Scopes for Custom Associations

You can extend the centralized pattern by adding new scopes that target specific associations. For example, to eager-load agent data alongside properties:

```ruby

# app/models/concerns/listed_property/searchable.rb

scope :with_agent, -> { includes(:agent) }

```

Callers can then chain this scope with existing ones to fetch all required data in two SQL statements:

```ruby
properties = ListedProperty::Searchable
               .properties_search(params)
               .with_eager_loading
               .with_agent

```

## Controller-Level N+1 Prevention

Every controller that renders collections or detail pages consistently applies eager-loading to the relations the view requires. This pattern appears throughout the `tenant_admin` and `site_admin` namespaces, ensuring views can iterate over associations without firing extra queries per record.

Key implementations include:

- **TenantAdmin::SubdomainsController** (`set_subdomain`): Uses `includes(website: { user_memberships: :user })` to load Subdomain → Website → User memberships → User in a single query chain.
- **TenantAdmin::SubscriptionsController** (`index`): Calls `Pwb::Subscription.includes(:website, :plan)` to pre-load subscription plan data.
- **SiteAdmin::UsersController** (`index`): Applies `current_website.users.includes(:user_memberships)` for admin user listings.
- **SitemapsController** (`show`): Uses `includes(prop_photos: { image_attachment: :blob })` to generate sitemap XML without N+1 penalties on ActiveStorage attachments.

The [`app/controllers/concerns/site_admin_indexable.rb`](https://github.com/etewiah/property_web_builder/blob/main/app/controllers/concerns/site_admin_indexable.rb) helper further enforces this pattern by dynamically injecting `includes` based on controller-declared `indexable_includes` arrays, standardizing the approach across resourceful controllers.

### Limiting Result Sets for Bulk Operations

When loading large datasets for dashboard indices, controllers combine eager-loading with strict record limits to control memory consumption. The `TenantAdmin::PagesController#index` demonstrates this defensive pattern:

```ruby

# app/controllers/tenant_admin/pages_controller.rb

@pages = Pwb::Page.unscoped
                .includes(:website)
                .order(created_at: :desc)
                .limit(100)   # ← avoid full‑table scan

```

This same pattern appears in `TenantAdmin::PropsController` and `TenantAdmin::ContentsController`, ensuring that bulk admin views remain responsive regardless of table size.

### Practical Controller Implementation

When implementing a new index action, follow the established pattern from `TenantAdmin::AgentsController`:

```ruby

# app/controllers/tenant_admin/agents_controller.rb

class TenantAdmin::AgentsController < TenantAdminController
  def index
    @agents = Pwb::Agent
                .includes(:website, :user)            # eager‑load

                .order(created_at: :desc)
                .limit(50)                           # limit result set

  end
end

```

The view can safely call `agent.website.name` and `agent.user.email` without triggering additional database queries.

## Runtime Detection with Bullet

During development, PropertyWebBuilder integrates the **Bullet** gem to surface any missed N+1 queries immediately. Configured in [`config/environments/development.rb`](https://github.com/etewiah/property_web_builder/blob/main/config/environments/development.rb), Bullet raises alerts when views iterate over associations that have not been eager-loaded:

```ruby

# config/environments/development.rb

Bullet.enable        = ENV.fetch("BULLET_ENABLED", "true") == "true"
Bullet.alert         = ENV.fetch("BULLET_ALERT", "false") == "true"
Bullet.bullet_logger = ENV.fetch("BULLET_LOGGER", "true") == "true"
Bullet.console       = ENV.fetch("BULLET_CONSOLE", "true") == "true"
Bullet.rails_logger  = ENV.fetch("BULLET_RAILS_LOGGER", "true") == "true"
Bullet.add_footer    = ENV.fetch("BULLET_FOOTER", "true") == "true"
Bullet.add_safelist type: :unused_eager_loading,
                    class_name: "Pwb::SiteAdmin::UserSearch"

```

The configuration logs warnings to the browser console and Rails logger, giving developers instant feedback. The safelist prevents false positives for known acceptable patterns, such as unused eager loading in admin search interfaces. Comments in [`app/helpers/pwb/component_helper.rb`](https://github.com/etewiah/property_web_builder/blob/main/app/helpers/pwb/component_helper.rb) further document why specific fields are eager-loaded to satisfy Bullet requirements.

## Composable Search with Pre-loaded Relations

The `ListedProperty::Searchable` module exposes a `properties_search` class method that returns an `ActiveRecord::Relation`. Because the default scopes (`all.visible.for_sale` or `all.visible.for_rent`) return relation objects, you can chain `with_eager_loading` without executing additional queries:

```ruby

# Example usage in a controller or service

search = ListedProperty::Searchable.properties_search(**params)
search = search.with_eager_loading   # load website, listings, photos in one go

```

This composable API keeps search logic decoupled from loading logic while guaranteeing that the final result set arrives with all necessary associations pre-loaded.

## Summary

- **Centralize eager-loading scopes** in model concerns like `ListedProperty::Searchable` to provide reusable `includes` bundles for common use cases.
- **Apply `includes` consistently** in every controller action that renders collections, nesting associations as needed (e.g., `includes(website: { user_memberships: :user })`).
- **Limit result sets** using `.limit(n)` on bulk index actions to prevent memory bloat on large tables.
- **Enable Bullet in development** to catch N+1 queries immediately via browser alerts and Rails logs, adding safelists only for verified acceptable patterns.
- **Chain scopes composably** when building search interfaces, allowing `properties_search` results to leverage `with_eager_loading` without query penalties.

## Frequently Asked Questions

### What causes N+1 queries in ActiveRecord?

N+1 queries occur when application code iterates over a collection of records and accesses an associated record that was not loaded in the initial query, forcing the database to execute an additional query for each iteration. PropertyWebBuilder prevents this by declaring `includes` in model scopes and controller queries before collections reach the view, ensuring associations are pre-loaded.

### How does the Bullet gem configuration work in PropertyWebBuilder?

Bullet monitors query patterns during development and raises alerts when it detects N+1 queries or unused eager loading. In [`config/environments/development.rb`](https://github.com/etewiah/property_web_builder/blob/main/config/environments/development.rb), Bullet is enabled with environment variable toggles and configured to log to the Rails logger and browser console. The configuration includes a safelist for `Pwb::SiteAdmin::UserSearch` to suppress false positives in admin search interfaces where eager loading might appear unused due to conditional view logic.

### When should I use `includes` versus `joins` for query optimization?

Use `includes` when you need to access associated data in views or subsequent operations, as it triggers eager loading that prevents N+1 queries. Use `joins` when you only need to filter by associated data without instantiating the associated objects. PropertyWebBuilder consistently uses `includes` in collection controllers—evident in `SitemapsController` and `SubscriptionsController`—to ensure views can safely traverse associations without database penalties.

### How do I extend eager loading for custom associations in this codebase?

Define a new scope in the relevant model concern, such as adding `scope :with_agent, -> { includes(:agent) }` to `ListedProperty::Searchable`. In your controller, chain this scope alongside existing ones like `with_eager_loading`. Follow the pattern in `TenantAdmin::AgentsController` by applying `.includes(:website, :user)` directly in the index action, and always combine with `.limit(n)` when rendering large collections.