All posts
Anshuman Praharaj

Building a Reliable Payment and Billing System with Razorpay


During my internship, I built a payment and billing system integrated with Razorpay.

The flow looked straightforward:

User selects a plan → Order is created in Razorpay → Payment is completed → Subscription is activated with pro-rata calculation → Invoice is generated → Transaction is marked as INVOICE_GENERATED.

At first glance, this seems like a simple payment integration. Create an order, collect payment, update the subscription, and generate an invoice.

But after working on it, I realized that the integration itself is not the difficult part.

The real challenge is ensuring reliability.

Why Payment Systems Are Different

Payments are a core business function. Any inconsistency can directly impact revenue and user trust.

In an ideal world, every user completes the payment flow perfectly and every callback arrives on time.

In reality, things break.

Users close browser tabs midway through the payment flow. Network requests fail. Callbacks get delayed. Sometimes webhooks never arrive.

A payment system has to handle all of these situations and still arrive at the correct final state.

The Reliability Problem

Imagine a user successfully completes a payment, but closes the browser before the frontend receives confirmation.

Or imagine Razorpay sends a webhook, but your server is temporarily unavailable.

If the system depends entirely on a single event, transactions can end up stuck in an incorrect state.

For a payment platform, that is unacceptable.

How I Solved It

To make the system reliable, I implemented two reconciliation mechanisms.

1. Webhook-Based Updates

The first layer was a webhook handler.

Whenever Razorpay sent payment events, the webhook updated the corresponding transaction in real time.

This allowed the system to react immediately when payments succeeded or failed.

2. Scheduled Reconciliation Job

The second layer was a scheduled reconciliation process.

The job periodically picked up transactions that were still marked as PENDING.

For each transaction, it queried Razorpay directly to fetch the latest payment status and then updated the system accordingly.

This acted as a safety net.

Even if a webhook was missed or a user left the payment flow midway, the reconciliation job ensured the transaction would eventually reach the correct state.

Why This Approach Worked

Using both webhooks and reconciliation jobs provided a much more reliable payment system.

The webhook handled the common case and provided real-time updates.

The reconciliation job handled failures and edge cases.

Together, they ensured that transaction data remained accurate even when external systems or users behaved unexpectedly.

Key Lessons I Learned

Building this system taught me several important lessons about payment infrastructure.

Webhooks Are Best-Effort

Never assume every webhook will arrive successfully.

Always have a fallback mechanism to verify and reconcile payment states.

Idempotency Is Critical

When money is involved, duplicate processing can create serious issues.

Every operation should be designed so that executing it multiple times produces the same result.

Track Every State Change

Every transaction should have a clear audit trail.

This makes debugging easier and helps explain exactly what happened during a payment flow.

Billing Logic Has Hidden Complexity

Pro-rata calculations sound simple until you start handling plan upgrades, downgrades, billing cycles, and edge cases.

A lot of the complexity lives in the details.

Before building this project, I thought payment integrations were mostly about connecting APIs.

After working on a real billing system, I learned that reliability and correctness matter far more than the integration itself.

Creating an order in Razorpay is easy.

Making sure every transaction eventually reaches the correct state, even when users, networks, and external systems fail, is where the real engineering happens.