ruby 99 lines · 3 tabs

Graceful Degradation: Feature-Based Rescue

Shared by codesnips Jan 2026
3 tabs
class CircuitBreaker
  class CircuitOpenError < StandardError; end

  INCREMENT = <<~LUA.freeze
    local n = redis.call('INCR', KEYS[1])
    if n == 1 then redis.call('EXPIRE', KEYS[1], ARGV[1]) end
    return n
  LUA

  def initialize(name, threshold: 5, cooldown: 30, redis: Redis.current)
    @name = name
    @threshold = threshold
    @cooldown = cooldown
    @redis = redis
  end

  def run
    raise CircuitOpenError, @name if open?

    result = yield
    reset!
    result
  rescue StandardError
    trip!
    raise
  end

  private

  def open?
    failure_count >= @threshold
  end

  def failure_count
    @redis.get(key).to_i
  end

  def trip!
    @redis.eval(INCREMENT, keys: [key], argv: [@cooldown])
  end

  def reset!
    @redis.del(key)
  end

  def key
    "circuit:#{@name}"
  end
end
3 files · ruby Explain with highlit

This snippet shows how a Rails app keeps rendering useful pages even when a downstream dependency (a recommendation service, a pricing API) is slow or broken. Instead of letting one failing call take down a whole request, each risky feature is wrapped so it can fail independently and fall back to a degraded-but-acceptable result. The pattern combines a circuit breaker, a per-feature rescue helper, and a controller that composes both.

In CircuitBreaker, the breaker tracks consecutive failures for a named dependency in Redis. run short-circuits with CircuitOpenError once failure_count crosses @threshold, so a struggling service is left alone to recover rather than being hammered by every request. The INCREMENT Lua script increments and sets a TTL atomically, which avoids the race where two processes both read a stale count and neither renews the expiry. When the breaker is open the block is never invoked, turning a slow timeout into an instant, cheap failure.

FeatureRescue is the ergonomic layer. with_fallback names a feature, runs the breaker-wrapped block, and on any StandardError returns the supplied fallback instead of raising. It records what degraded in a per-request Set (degraded_features) and reports the exception to the error tracker, so a fallback never becomes an invisible silent failure. That distinction matters: graceful degradation should still be observable, otherwise a permanently broken feature looks healthy.

ProductsController#show demonstrates composition. The core product load is intentionally not wrapped — if the product itself cannot be fetched, the page genuinely cannot render, so that error should propagate to a normal 500 or 404. Only the optional sections (recommendations, live_pricing) go through with_fallback, each with its own breaker name and a sensible default such as [] or a cached price. The view can then check degraded?(:recommendations) to hide or annotate a section.

The key trade-off is deciding which failures are tolerable: wrapping something essential in a fallback hides real outages, while wrapping nothing makes the app brittle. The rule of thumb is to degrade features that are additive and keep hard failures for data the page cannot exist without.


Related snips

ruby
class CommentsController < ApplicationController
  before_action :set_post

  def create
    @comment = @post.comments.build(comment_params)

System test: asserting Turbo Stream responses

rails hotwire turbo
by codesnips 4 tabs
ruby
class Post < ApplicationRecord
  belongs_to :author, class_name: 'User'
  has_many :comments, dependent: :destroy

  scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
  scope :draft, -> { where(published_at: nil) }

ActiveRecord scopes for reusable query logic

rails activerecord patterns
by Alex Kumar 1 tab
typescript
export interface RetryOptions {
  retries: number;
  baseMs: number;
  maxMs: number;
  signal?: AbortSignal;
  onRetry?: (attempt: number, delay: number, err: unknown) => void;

Exponential backoff with jitter for retries

typescript reliability retry
by codesnips 2 tabs
ruby
module Api
  module V1
    class UsersController < BaseController
      def show
        user = User.includes(:profile).find(params[:id])

ETags for conditional requests and caching

rails caching http-caching
by Alex Kumar 1 tab
ruby
class PostsController < ApplicationController
  def index
    @posts = Post.includes(:author)
                 .order(created_at: :desc)
                 .page(params[:page])
                 .per(10)

Turbo Frames: infinite scroll with lazy-loading frame

rails turbo hotwire
by codesnips 4 tabs
ruby
require "csv"

class PeopleCsvStream
  include Enumerable

  HEADERS = %w[id full_name email signed_up_at plan].freeze

Resilient CSV Export as a Streamed Response

rails performance streaming
by codesnips 3 tabs

Share this code

Here's the card — post it anywhere.

Graceful Degradation: Feature-Based Rescue — share card
Link copied