Facebook iconFrom Razorpay To Global: Our Payment Gateway Journey
Blogs/Technology

From Razorpay To Global: Our Payment Gateway Journey

Oct 30, 20245 Min Read
Written by Aravindan Udayasekaran
From Razorpay To Global: Our Payment Gateway Journey Hero

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

Partner with Us for Success

Experience seamless collaboration and exceptional results.

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.

Partner with Us for Success

Experience seamless collaboration and exceptional results.

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.

Author-Aravindan Udayasekaran
Aravindan Udayasekaran

With 11+ years experience, shaping cutting-edge apps. I thrive on transforming concepts into functional and efficient systems. Catch me playing cricket and belting out karaoke in my leisure time

Phone

Next for you

Flutter Architecture Guide 2025: 5 Patterns to Build Efficient Apps Cover

Technology

Apr 12, 20254 min read

Flutter Architecture Guide 2025: 5 Patterns to Build Efficient Apps

Are you building a Flutter app but not sure how to organize your code for long-term growth? A recent report by Statista found that 65% of developers believe a strong app architecture directly boosts productivity and reduces bugs. If you've been Googling terms like “flutter app architecture or state management patterns”, you’re on the right track. This blog will walk you through five practical architecture patterns that are working well in 2025, covering when to use each, why it matters, and h

13 Flutter Performance Optimization Techniques in 2025 Cover

Technology

Apr 11, 20255 min read

13 Flutter Performance Optimization Techniques in 2025

Flutter is fast by default, but like any framework, poorly written code can drag things down. Whether you're building a complex mobile app or a lightweight MVP, performance matters. In this article, we'll explore 13 essential Flutter performance optimization techniques to help you build high-performing applications that keep users satisfied and engaged. Let’s get into the nitty-gritty of optimizing your Flutter app, the smart way. 13 Flutter Performance Optimization Techniques To Know in 2025

State Management in Flutter: 7 Approaches to Know (2025) Cover

Technology

Apr 10, 20253 min read

State Management in Flutter: 7 Approaches to Know (2025)

Are you finding it hard to manage state in Flutter applications? With the growing complexity of mobile apps, state management in Flutter has become one of the most critical aspects for developers. According to a survey by Stack Overflow, Flutter is now one of the most popular frameworks used by developers globally.  Effective state management can make or break an app’s performance and scalability. In this guide, we’ll dive into the importance of state management in Flutter and explore the most