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.