ruby 90 lines · 3 tabs

Safer Feature Flagging: Cache + DB Fallback

Shared by codesnips Jan 2026
3 tabs
class FeatureFlag < ApplicationRecord
  validates :key, presence: true, uniqueness: true
  validates :percentage, inclusion: { in: 0..100 }

  after_commit :expire_cache

  def enabled_for?(actor_id)
    return false unless enabled
    return true if percentage >= 100
    return false if percentage <= 0

    bucket(actor_id) < percentage
  end

  def cache_key
    self.class.cache_key_for(key)
  end

  def self.cache_key_for(flag_key)
    "feature_flag/v2/#{flag_key}"
  end

  private

  def bucket(actor_id)
    seed = "#{key}:#{actor_id}"
    Digest::MD5.hexdigest(seed).to_i(16) % 100
  end

  def expire_cache
    Rails.cache.delete(cache_key)
  end
end
3 files · ruby Explain with highlit

This snippet shows a feature flag system built around a read path that stays fast under normal load but degrades gracefully when the cache is unavailable. The design goal is that a flag lookup should never take down a request: it prefers the cache, falls back to the database, and finally falls back to a static default so that even a total datastore outage produces a safe answer.

In FeatureFlag model, each flag is a durable Postgres row keyed by a unique key, with a boolean enabled and an integer percentage for gradual rollouts. The enabled_for? method centralizes the rollout decision: a fully disabled flag short-circuits to false, a 100% flag is a simple boolean, and anything in between is bucketed deterministically. The bucketing uses an MD5 digest of the flag key plus the actor id, converted to an integer mod 100, so the same actor always lands in the same bucket for a given flag — that stability is what makes percentage rollouts feel consistent rather than flickering on every request. An after_commit hook calls expire_cache so writes invalidate the cached copy immediately.

In FlagResolver service, enabled? implements the layered read. It asks Rails.cache.fetch first, and the block behind that fetch hits the database only on a miss, memoizing the row for CACHE_TTL. The whole thing is wrapped in a rescue that logs and returns a hardcoded DEFAULTS value if both cache and DB raise — this is the crucial resilience property. read_flag caches even the absence of a flag (as nil) to avoid a stampede of DB lookups for unknown keys, a common source of accidental load.

In ApplicationController concern, feature_enabled? wires the resolver into request handling using current_user&.id as the actor, and require_feature! turns a flag into a routing guard that raises ActionController::RoutingError when the feature is off. The trade-off is mild staleness: after a flag flips, cached copies live up to CACHE_TTL on nodes that did not process the invalidation, which is acceptable for flags but not for security-critical gates. This pattern fits any system that reads flags on the hot path and cannot tolerate a cache outage becoming a full outage.


Related snips

Share this code

Here's the card — post it anywhere.

Safer Feature Flagging: Cache + DB Fallback — share card
Link copied