From Razorpay To Global: Our Payment Gateway Journey
When we initiated our project, our vision was clear: create an application tailored for the Indian audience. Naturally, we chose RazorPay as our payment gateway, given its popularity in India. Based on our initial target audience, this decision set the stage for an exciting journey of scaling and adaptation in our payment integration process.
As our project's scope expanded beyond Indian borders, we faced a critical question: How could we transform our tightly coupled RazorPay integration into a scalable and reliable system accommodating various payment gateways?
Initial Implementation
Our first implementation was tightly coupled with RazorPay. We created specific database tables for RazorPay payments and webhooks. Here's what our initial schema looked like:
create_table "razorpay_payments", charset: "latin1", force: :cascade do |t|
t.bigint "booking_id", null: false
t.string "razorpay_order_id"
t.string "razorpay_payment_id", null: false
t.string "razorpay_signature_id"
t.string "razorpay_refund_id"
t.string "razorpay_transfer_id"
t.decimal "amount", precision: 10, scale: 3
t.decimal "transfered_amount", precision: 10, scale: 3
t.string "status"
t.json "payload"
t.text "error_description"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "razorpay_webhooks", charset: "latin1", force: :cascade do |t|
t.string "razorpay_payment_id"
t.text "webhook_data"
t.integer "event"
t.string "razorpay_order_id"
t.bigint "payment_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
The Expansion: Adding More Gateways
As we expanded to include Stripe and PayPal, we simply replicated this approach, creating separate tables for each gateway and duplicating much of the code with minor modifications.
The Problems Emerge
This method quickly showed its flaws:
- We had to query multiple tables to verify payments across different entities (Bookings, Donations, etc.).
- Code duplication led to maintenance nightmares.
- Adding new gateways meant creating new tables and more duplicate code.
GMO Integration
The integration of GMO, a Japanese payment gateway, was our turning point. We realized our code lacked scalability and reliability. It was time to step back, rethink our architecture, and embrace scalable coding practices.
Redesigning The Architecture
We went back to the drawing board and redesigned our payment system using the following design patterns:
- Factory Pattern
- Strategy Pattern
Key Components of the New Architecture
1. Generic Payment Table: We introduced a single, generic table for all payment gateways, current and future.
create_table "payments", charset: "utf8mb3", force: :cascade do |t|
t.float "amount", null: false
t.string "reference"
t.datetime "paid_at"
t.integer "gateway", default: 0, null: false
t.string "payment_type"
t.integer "status"
t.string "entity_type"
t.bigint "entity_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "invoice_number"
t.json "gst"
t.json "breakup"
t.index ["entity_type", "entity_id"], name: "index_payments_on_entity"
end
2. Unified Webhook Table: A common table to store callbacks from all gateways.
create_table "payments_webhooks", charset: "utf8mb3", force: :cascade do |t|
t.string "event", null: false
t.json "payload", null: false
t.integer "gateway", default: 0, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
3. Factory Class: This class returns an instance of the appropriate gateway implementation based on input.
# factory.rb
class Payments::Factory
def self.interface(gateway)
case gateway
when 'razorpay'
return Payments::Razorpay::Implementation.new
when 'stripe'
return Payments::Stripe::Implementation.new
end
end
end
Class: An abstract class defining methods each gateway implementation must include.
# interface.rb
module Payments::Interface
def process_webhook(event, payload)
raise NotImplementedError
end
def create_order(options)
raise NotImplementedError
end
def payment_events
raise NotImplementedError
end
def parse_event(payload)
raise NotImplementedError
end
def verify_webhook(params)
raise NotImplementedError
end
def crud_entity(params)
raise NotImplementedError
end
end
5. Gateway-Specific Implementations: Separate classes for each gateway, implementing the methods defined in the interface.
# razorpay/implementation.rb
require 'razorpay'
class Payments::Razorpay::Implementation
include Payments::Interface
attr_reader :operation, :entity_id, :entity, :event, :payload
PAYMENT_STATUSES = { captured: 'success', failed: 'failed' }
PAYMENT_EVENTS = %w[payment.captured subscription.charged]
def process_webhook(event, payload)
@event, @payload = event, payload
process_payment if PAYMENT_EVENTS.include? event
end
def process_payment
entity = payload.dig(:payload, :payment, :entity)
payment_object = {
reference: entity.dig(:id), amount: (entity.dig(:amount) / 100),
status: PAYMENT_STATUSES[entity.dig(:status).to_sym],
paid_at: Time.at(entity.dig(:created_at)).to_datetime,
entity_type: entity.dig(:notes, :entity_type),
entity_id: entity.dig(:notes, :entity_id)
}
'Payment'.constantize.create payment_object
end
def create_order(options)
puts 'Razorpay create_order'
payload = { amount: options[:amount], currency: options[:currency].upcase,
receipt: "booking_rcpt_#{Time.now.to_i}", payment_capture: "1", notes: options[:meta]}
Razorpay::Order.create(payload).id
end
def parse_event(payload)
payload.deep_symbolize_keys!.dig(:event)
end
def verify_webhook(params)
Razorpay::Utility.verify_webhook_signature(params[:webhook_body], params[:signature], params[:secret])
end
end
The New Workflow: How It All Comes Together
With this new architecture in place, processing payments and webhooks became a unified process, regardless of the gateway used. Here's how it works:
- To process any method of the interface, we invoke the factory class with the gateway name, get the instance, and invoke the method from there.
Payments::Factory.interface('razorpay').create_order(params)
- The factory class returns the appropriate gateway implementation.
- The method is executed through gateway-specific implementation.
Adding New Gateways: A Plug-and-Play Approach
With our new architecture, adding a new gateway became as simple as creating a new implementation file and updating the factory class. For example, to add Stripe:
# stripe/implementation.rb
require 'stripe'
class Payments::Stripe::Implementation
include Payments::Interface
attr_reader :operation, :entity_id, :entity, :event, :payload
def process_webhook(event, payload)
# Stripe-specific implementation
end
def process_payment
# Stripe-specific implementation
end
def create_order(options)
# Stripe-specific implementation
end
def parse_event(payload)
# Stripe-specific implementation
end
def verify_webhook(params)
# Stripe-specific implementation
end
end
Then, update the factory:
# factory.rb
class Payments::Factory
def self.interface(gateway)
case gateway
when 'razorpay'
return Payments::Razorpay::Implementation.new
when 'stripe'
return Payments::Stripe::Implementation.new
end
end
end
The Benefits of Our New Approach
1. Scalability: Adding new gateways became a plug-and-play process.
2. Maintainability: Centralized logic reduced code duplication.
3. Flexibility: Switching between gateways requires minimal code changes.
Consistency: A unified approach to handling payments across all gateways.
Our journey from a single, tightly coupled payment gateway to a flexible, multi-gateway system taught us valuable lessons in scalable architecture. By embracing design patterns and thinking ahead, we transformed a potential disaster into a robust, future-proof payment system.
This experience reinforced the importance of scalable coding practices, especially when dealing with critical components like payment processing. As our project continues to grow globally, we're now confident in our ability to adapt to new payment gateways and market requirements.
Remember, when building systems that may need to scale, always consider future expansion possibilities. A little extra effort, in the beginning, can save countless hours of refactoring and reduce technical debt in the long run.
Frequently Asked Questions
1. How did the team transition from a single payment gateway to multiple gateways?
They redesigned their architecture using the Factory and Strategy patterns, creating a generic payment table and unified webhook table to accommodate multiple gateways.
2. What were the main problems with the initial implementation?
The initial approach led to code duplication, maintenance issues, and difficulty in querying payments across entities. Adding new gateways required creating new tables and duplicating code.
3. What are the benefits of the new payment system architecture?
The new approach offers improved scalability, maintainability, flexibility, and consistency. It allows easy addition of new gateways and provides a unified method for handling payments across all gateways.
Need expert help?
We offer top-notch product development services, turning ideas into market-ready solutions. Our team builds custom, scalable apps using the latest tech and best practices. Let's create something amazing that'll take your business to new heights.