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

5 Rust Features I Wish JavaScript and Ruby Had Cover

Technology

Jan 22, 20258 min read

5 Rust Features I Wish JavaScript and Ruby Had

Recently, I have been tinkering with an RPi Zero 2 W board: making it serve a small app on my local network, running a lightweight LLM model to see how long it takes it to respond to a prompt (16 minutes 😢), and just the kind of experiments that you would run on a microcomputer of that scale. Being a full-stack developer, I wondered if I could use such a low-spec microcomputer as a server for a small internal tool. My first thought was to create that app in Ruby on Rails, and my second thought

New React 19 Features You Shouldn’t Miss Out Cover

Technology

Jan 22, 20254 min read

New React 19 Features You Shouldn’t Miss Out

Struggling with complex React development challenges in 2025? As a front-end developer who's been in the React trenches for years, I'm thrilled about React 19's release. With innovative features designed to streamline common pain points, this update promises to transform how we build web applications. The features introduced in this version tackle many long-standing challenges developers face daily. While React 19 brings numerous improvements, I want to focus on the React 19 features that will

How to Integrate Google Maps into Your Flutter App Cover

Technology

Nov 20, 20244 min read

How to Integrate Google Maps into Your Flutter App

In our smartphone-driven world, location services are transforming the app landscape. The global location-based services market is projected to hit a staggering reach USD 129.71 billion by 2032, exhibiting a CAGR of 19.5% during the forecast period (2024-2032) This explosion stems from our smartphone addiction and hunger for personalized experiences. It's only natural that developers are rushing to integrate Google Maps into their apps, aiming to deliver an engaging, user-friendly mapping experi