The Problem

In the fast-paced world of e-commerce, user experience is crucial. One critical aspect of delivering a seamless experience is optimizing the performance of your checkout page and avoiding service disruptions.

Traditional synchronous processing can lead to delays, resource bottlenecks and ultimately - unhappy customers.

That is especially true when the controller action for handling the checkout interacts with multiple third-party services, like payment processing, inventory management, email sending and others.

A spike in user checkouts should be a good thing - your customers ready to buy with their credit cards in their hands, and the last thing we want is them to see is an unresponsive site, and you waking up to a bunch of Internal Server Errors in your error logging service dashboard.

In this blog post, we'll explore one way of solving it by using the resources you are probably already using:

  • WebSockets with AnyCable (a more performant option to ActionCable)
  • Background Jobs with Sidekiq
  • Redis as the glue between them.

The Challenge: Blocking HTTP Requests

In a typical checkout process, there are various tasks that need to be performed, such as order processing, inventory updates, payment processing, email/sms notifications. These tasks, if executed synchronously, can lead to blocking HTTP requests, resulting in slower response times and potential timeouts.

The Solution: Parallel Processing with WebSockets and Background Jobs

To overcome the limitations of synchronous processing, we can leverage the power of WebSockets and background jobs. WebSockets provide a full-duplex communication channel over a single, long-lived connection, enabling real-time data exchange between the client and the server. Background jobs, managed by tools like Sidekiq, allow time-intensive tasks to be offloaded - queued and processed asynchronously in the background.

Step 1: Integrate AnyCable for WebSocket Connections

https://docs.anycable.io/rails/getting_started

Step 2: Install & Setup Sidekiq for Background Processing

https://github.com/sidekiq/sidekiq/wiki/Getting-Started

Step 3: Offload Checkout Actions to the Background

Move the service calls, and overall business logic from the controller to the CheckoutJob and just enqueue that job in the controller action.

# app/controllers/carts_controller.rb
class CartsController < ApplicationController
  def checkout
    CheckoutJob.perform_async(@cart.id, checkout_params)
    head :ok
  end
end

Step 4: Set up a CheckoutChannel

# frozen_string_literal: true
class CheckoutChannel < ApplicationCable::Channel
  def subscribed
    stream_for current_user
  end

  def unsubscribed
    stop_all_streams
  end
end

Step 5: Broadcast updates from your background job

# app/jobs/checkout_job.rb
class CheckoutJob
  include Sidekiq::Worker
  # Optional locking to avoid duplicate processing
  sidekiq_options queue: :high_priority, lock: :until_executed, retry: false

  def perform(cart_id, params)
    # Perform order processing logic here moving the service calls and business logic from the controller

    # ...

    # Finally, broadcast the update to the client depending on the result of your flow, e.g.:
    CheckoutChannel.broadcast_to(
      cart.user,
      type: :success,
      payload: {
        message: 'Your order has been proccessed successfully!',
        redirect: true,
        # ...
      }
    )
  end
end

6. Connect your client side JavaScript and respond to events

// app/assets/javascripts/channels/checkout_channel.js
import consumer from "./consumer";

consumer.subscriptions.create("CheckoutChannel", {
  received(data) {
    // Update the UI with real-time data
    // For example, show progress bars, remove spinner, redirect, etc.
  },
});

Conclusion

By making this type of optimization, you can ensure that the worst case for a spike in user traffic and requests will only have users waiting until their enqueued job gets executed, while having real-time feedback from your app, instead of it breaking and losing a sale.