From Razorpay To Global: Our Payment Gateway Journey

From Razorpay To Global

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:

  1. We had to query multiple tables to verify payments across different entities (Bookings, Donations, etc.).
  2. Code duplication led to maintenance nightmares.
  3. 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:

  1. 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)
  1. The factory class returns the appropriate gateway implementation.
  2. 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.