Skip to main content

How do I make my Stripe Charges more robust [Background Worker & Idempotent]?

Rails • Asked by Taylor Cooney

One thing I don't want to have to deal with are refunds because a customer happened to get charged twice. I want to ensure that I've designed my Rails app to be robust in the face of failure before I release it to production.

What is the best way to process Charge requests in the background? Currently I'm using sidekiq to invoke similar jobs (see Mailer example below). Moreover, should I be making idempotent requests? What's the best way to approach this? This way I don't have to worry about creating multiple charges, and also don't start blocking up the server with Charge requests.

Sidekiq Example
def send_job_post_email
JobMailer.send_job_post_email(user).deliver_later
end

JobsController < ApplicationController
def create
@job = current_user.jobs.build jobs_params.merge(stripe_token: stripe_params["stripeToken"])
if @job.create_with_stripe(params[:stripeToken])
  if @job.save
    @order = Order.create(
      :job_id =&gt; @job.id,
      :stripe_token =&gt; @job.stripe_token
    )
  end
  redirect_to job_order_path(@job, @order), notice: 'Registration was successfully created.'
else
  render_form
end

end


Job < ApplicationRecord
def create_with_stripe(token)
Stripe.api_key = Rails.application.secrets.stripe_secret_key
if valid?
  Stripe::Charge.create(
    :amount =&gt; 999,
    :currency =&gt; "cad",
    :source =&gt; token
  )
else
  false
end

rescue Stripe::CardError, Stripe::InvalidRequestError => e
errors.add :base, "Whoops! We were unable to process your card. #{e.message}"
false
end


I currently interact with Stripe using small, one-off Service objects. Can be run using any background worker. 
I also set an `idempotency_key`, with simply `SecureRandom.uuid`

Thanks for the reply @jack...I'm using Charge objects. How did you (or would I) go about setting the Service objects with Sidekiq to process in the background? I'm familiar with how Sidekiq and background jobs work, but I ask because the examples online show you having to poll the connection, which is over my head.

Moreover, how are you setting the idempotency_key? Is it simple as setting it in the Object and it automagically works?

Chris, let me know if you have any insight on this with steps that I can implement.


Bump...I feel this is important to processing charges in a logical manner

Taylor,

You really don't need to poll for the payment and respond to the customer that it was a success. I think most customers realize that when you input your card information, there's going to be a charge that happens at some point, and most don't really care for a confirmation that the charge went through (within your interface). This means you can just capture the card token and process the charge in the background, if successful update your Payment model (if keeping invoice data) that payment went through. If it fails, prompt the customer with an email to update card details and try the charge again.

In one implementation we charge customers on a nightly job based on a pre-defined schedule. Stripe never struggles with us throwing requests at it like crazy at midnight. 

You're on the right track with moving the processing of the charge to a background job. I guess you need to answer whether or not the confirmation is important to your on-boarding flow.


Thanks for the response eminkel. In regards to processing the job in a background worker, can I enqueue the job just as I would any other - is there anything special that I need to do? If I understand correctly, I can just skip the ajax call made to check the status of the payment, as customers expect there to be processed at some point.

Hey Taylor,

Yeah, you can process the payment like any normal background job. Don't allow retry if the job fails (for fear of multiple payments), and return and update some sort of charge id on success to your database.

Awesome, 

That helps a bunch! I know I can disable retry support for a particular worker by appending:
sidekiq_options :retry => false # job will be discarded immediately if failed

In my controller, I create an Order object with the :stripe_token and :job_id if @job.save. If I add the :charge_id to the Order as well, will that be sufficient or is there more I can do? (See the code block in the main post)

Sounds like plenty to get the charge done to me. You can add additional logic as needed if your requirements change.

Login or Create An Account to join the conversation.

Subscribe to the newsletter

Join 22,346+ developers who get early access to new screencasts, articles, guides, updates, and more.

    By clicking this button, you agree to the GoRails Terms of Service and Privacy Policy.

    More of a social being? We're also on Twitter and YouTube.