ruby 114 lines · 4 tabs

Redeem Discount Codes in Rails with a Locking Service and Row-Level Guards

Shared by codesnips Sep 2026
4 tabs
class DiscountCode < ApplicationRecord
  has_many :redemptions, dependent: :destroy

  enum discount_type: { percentage: 0, fixed: 1 }

  validates :code, presence: true, uniqueness: { case_sensitive: false }
  validates :value_cents, numericality: { greater_than: 0 }
  validates :usage_limit, numericality: { greater_than: 0 }, allow_nil: true

  before_validation { self.code = code.to_s.strip.upcase }

  def active?(at: Time.current)
    enabled? &&
      (starts_at.nil? || starts_at <= at) &&
      (expires_at.nil? || expires_at >= at)
  end

  def usage_available?
    usage_limit.nil? || redemptions.count < usage_limit
  end

  def amount_for(subtotal_cents)
    raw =
      if percentage?
        (subtotal_cents * value_cents / 100.0).round
      else
        value_cents
      end
    [raw, subtotal_cents].min
  end
end
4 files · ruby Explain with highlit

This snippet shows how a discount code is applied to an order using a redeemable model plus a service that enforces the business rules under concurrency. The core problem is race conditions: two shoppers can try to redeem the last available use of a code at the same time, and naive count checks let both succeed. The design pushes correctness down to the database with row locks and a redemption ledger.

In DiscountCode model, each code carries an optional usage_limit, a validity window (starts_at/expires_at), and a discount_type enum for percentage or fixed amounts. The active? predicate combines the enabled flag with the time window so the service has a single question to ask. amount_for computes the discount against a subtotal and clamps a fixed discount so it never exceeds the order total, avoiding negative charges. Redemptions are tracked through a has_many :redemptions association rather than a mutable counter, which makes each use auditable and lets a unique index prevent a customer from redeeming the same code twice.

In Redemption model, the join between a discount_code and an order records the amount_cents actually granted. The validates_uniqueness_of :order_id guard means an order can only ever hold one redemption, so re-running the apply flow is idempotent at the schema level.

In DiscountRedemptionService, call wraps everything in a transaction and immediately calls lock! on the code row, taking a SELECT ... FOR UPDATE. Holding that lock, it re-checks active? and compares redemptions.count against usage_limit, so the limit check and the insert happen atomically — no two concurrent callers can both pass the check. A Result struct communicates success or a symbolic failure reason instead of raising for expected outcomes, while a genuine RecordInvalid (such as a duplicate order redemption) is caught and mapped to :already_redeemed.

In OrdersController, the apply_discount action delegates to the service and branches on result.success?, keeping controller logic thin. The trade-off is that lock! serializes redemptions of a single hot code, but that contention is precisely what guarantees the usage limit is honored. This pattern fits any scarce-resource redemption — coupons, invites, seat allocations — where correctness matters more than raw throughput.


Related snips

Share this code

Here's the card — post it anywhere.

Redeem Discount Codes in Rails with a Locking Service and Row-Level Guards — share card
Link copied