In-Memory Caching
Authorization checks happen constantly — every controller action, every view helper, every allowed_to? call. When your policy rules hit the database, this adds up fast. Action Policy includes a multi-layered caching system to keep things snappy.
The N+1 Authorization Problem
Imagine rendering a product list with edit buttons:
<% @products.each do |product| %> <%= product.name %> <% if allowed_to?(:update?, product) %> <%= link_to "Edit", edit_product_path(product) %> <% end %><% end %>If update? queries the database (say, checking team membership or subscription status), you get one query per product. With 50 products, that’s 50 extra queries on every page load.
Three Layers of Caching
Action Policy solves this with three caching layers, applied automatically as you build up from basic usage to production-ready policies.
Layer 1: Per-Instance Memoization
The simplest layer is policy instance reuse. When you call authorized? or allowed_to? multiple times for the same record, Action Policy returns the same policy instance rather than creating a new one.
This is handled by ActionPolicy::Behaviours::Memoized, which is automatically included in Rails controllers. The memoization key uses policy_cache_key if available, then cache_key, then falls back to object_id:
# These reuse the same ProductPolicy instanceallowed_to?(:show?, product)allowed_to?(:update?, product) # same instanceallowed_to?(:destroy?, product) # same instanceLayer 2: Per-Rule Caching
Within a single policy instance, rule results are memoized. Once update? is evaluated, calling it again on the same instance returns the cached result immediately — no re-execution of the rule body.
This is built into ActionPolicy::Base via CachedApply. It means this view code:
<% if allowed_to?(:update?, product) %> <%= link_to "Edit", edit_product_path(product) %><% end %><% if allowed_to?(:update?, product) %> <%= link_to "Quick Edit", edit_product_path(product, quick: true) %><% end %>…evaluates update? only once per policy instance, regardless of how many times you call it.
Layer 3: Persistent Caching with cache
Layers 1 and 2 only last for the duration of a single request. For expensive rules that rarely change, you can persist results across requests using cache:
class ProductPolicy < ApplicationPolicy cache :update?
def update? # This DB query is cached across requests user.teams.exists?(product_id: record.id) endendThe cached result is stored in the policy cache store (an in-memory store by default) and reused until the cache key changes or the entry expires.
Adding Caching to ProductPolicy
Open . The current show? rule checks whether a product is published — a database query that runs on every page listing products:
def show? record.published? || record.user_id == user&.idendAdd cache :show? to persist this result across requests:
class ProductPolicy < ApplicationPolicy cache :show?
relation_scope do |relation| if user&.admin? relation else relation.where(published: true) end end
def index? true end
def show? record.published? || record.user_id == user&.id end
def create? user.present? end
alias_rule :new?, to: :create?
def update? user.present? end
alias_rule :edit?, :destroy?, to: :update?endHow the Cache Key Works
Action Policy builds cache keys from multiple components to ensure correctness:
"action_policy/{namespace}/{user_cache_key}/{record_cache_key}/{policy_class}/{rule}"For example:
"action_policy//users/42-20240315/products/7-20240310/ProductPolicy/show?"This means:
- Different users get different cache entries
- Updating a record (changing
updated_at) automatically invalidates its cache entries - Different rules are cached independently
Customizing policy_cache_key
If your model doesn’t use standard cache_key (or you want a more specific key), define policy_cache_key on the model:
class User < ApplicationRecord def policy_cache_key "#{id}/#{role}/#{updated_at.to_i}" endendThis is useful when the cache validity depends on fields that aren’t captured in the default cache_key (which uses id and updated_at).
Seeing It In Action
Verify caching in the Rails console:
$ bin/rails consolestore(dev)> product = Product.firststore(dev)> user = User.find_by(email_address: "user@example.com")
store(dev)> policy = ProductPolicy.new(product, user: user)=> #<ProductPolicy ...>
# First call — evaluates the rule and caches resultstore(dev)> policy.apply(:show?)=> true
# Second call — returns memoized result immediately (Layer 2)store(dev)> policy.apply(:show?)=> true
# Check the result objectstore(dev)> policy.result.cached?=> trueThe cached? method on the result tells you whether the rule result came from cache.
Now add cache :show? to your ProductPolicy using the solution button, or type it in yourself!
- Preparing Ruby runtime
- Prepare development database
- Starting Rails server