Stack guide

Rails and Stripe Connect: refunds, callbacks, background jobs

Rails makes refunds dangerously easy: one model callback fires and money moves. The same convenience scatters correctness — callbacks bypass review, ActiveJob retries execute blindly, and the service object pattern exists precisely because payment logic deserves explicit boundaries. Here is the Rails-native shape of doing it right.

Service object with both flags

Refunds belong in explicit services returning result objects, not model callbacks hiding side effects. Callback-triggered refunds are the audit blind spot: discover them via grep -rn "refunds.create" across app/models before assuming your inventory is complete.

class Refunds::Create
  def initialize(order:, attempt:)
    @order, @attempt = order, attempt
  end

  def call
    Stripe::Refund.create({
      charge: @order.stripe_charge_id,
      reverse_transfer: true,
      refund_application_fee: true,
    }, { idempotency_key: "refund-#{@order.id}-#{@attempt}" })
  end
end

ActiveJob retry semantics

retry_on without intent-scoped keys re-executes entire perform methods — including successful-partial scenarios. Keys derived from order-plus-intent collapse retries; live-state reads guard the 24-hour expiry tail. Sidekiq follows identical rules with different syntax.

Version pinning and schema drift

stripe-ruby updates occasionally shift object shapes detectors rely upon; pin versions, review changelogs deliberately, and let monitoring catch drift empirically — findings dropping to zero after an upgrade is itself a signal.

Common questions

Does this require a FeeGuard integration?

No — the page stands alone as stack guidance. FeeGuard observes your event stream externally rather than embedding in it.

Callbacks or services for refunds?

Services. Callbacks hide money movement from review and testing; explicitness is the whole point of boundaries in payment code.