Working with Payment Gateways: How to Build Maintainable Systems
If you've ever had to integrate payments into an app, you know it can quickly get tricky. Money is involved, networks drop requests, users retry buttons, and external services send async updates. If you don't structure your logic cleanly from day one, your system can easily become a headache to maintain. In this article, I want to share my understanding of working with payment gateways in the most maintainable way.
Here is how I break down the concepts, terminology, and core engineering principles to keep payment systems clean, safe, and reliable.
1.How Payments Actually Work Behind the Scenes One of the first things to understand is that our application doesn't normally move money directly from the customer's bank account into our database.
Instead, our app communicates with specialized organizations that handle the heavy lifting of payment processing. Some popular examples of companies offering payment infrastructure include Stripe, Paystack, Flutterwave, and Adyen.
To keep things clear in your head and in your codebase, it helps to distinguish between these terms:
Gateway: Securely transports payment information and requests between the customer/merchant and the payment processing infrastructure. Processor: Processes the transaction through the relevant payment networks and financial infrastructure. Payment Provider: A broader term for a company offering payment services under one roof—which can include the gateway, processing, fraud prevention tools, payouts, reporting, and more. Transaction: An operation representing a financial movement, or an attempted financial movement.
2.The Payment Lifecycle A payment doesn't happen instantaneously, it moves through a series of clear states. Thinking of a payment as a lifecycle makes your code much easier to reason about:
CREATED⟶PENDING⟶PROCESSING⟶SUCCESSFUL
CREATED: You've generated a payment record in your system. PENDING: The request has been handed off to the payment provider, and we're waiting for the customer or provider to act. PROCESSING: The provider is actively attempting to move the funds through the payment networks. SUCCESSFUL: The money has been verified and settled. When you manage payments as a explicit lifecycle, you prevent common bugs like fulfilling an order before the state actually hits SUCCESSFUL.
- Designing for the Real World: Idempotency, Webhooks & Verification Idempotency: Making Retries Safe Networks fail, connections drop, and users click "Pay" twice. This is where idempotency comes in.
Idempotency means that repeating the same operation does not produce additional unwanted effects.
We solve this by using an idempotency key—think of it as a unique label attached to an operation.
Important distinction: Idempotency doesn't mean "Don't send this request twice." Instead, it means "Make it safe if the request is sent multiple times."
When your provider or API receives the same idempotency key twice, it recognizes that it already handled that exact request and safely returns the existing result without charging the customer again.
Webhooks: Getting Notified Because payment processing happens asynchronously, you can't always wait around on a synchronous HTTP response.
A webhook is essentially an HTTP request sent by one system to another to notify it that something happened. The payment provider hits your server's endpoint to say, "Hey, transaction tx_123 just succeeded!"
Verification: Trust, but Verify Even when a webhook arrives or a customer gets redirected back to your success page, you shouldn't just take the client's word for it.
Verification means your server independently confirms the payment's actual status directly with the payment provider. Before you mark an order as paid in your database, your server makes a direct backend call to the provider to ask, "Is this payment truly valid and successful?"
- Keeping Your Data Safe: Atomicity & Financial Invariants Database Atomicity When a payment succeeds, you usually need to do multiple things: update the payment status, mark the order as paid, and decrement your product inventory.
Atomicity says that all operations succeed, or none of them are committed. This is one of the fundamental properties of database transactions. If your code updates the payment status but crashes right before updating inventory, an atomic transaction ensures the whole operation rolls back so your database never ends up in a broken, half-finished state.
What Is a Financial Invariant? An invariant is simply a rule that should always remain true in your system, no matter what happens.
When writing payment code, you should explicitly enforce financial invariants in your business logic. For example:
Refunded amount ≤ ≤ Paid amount: You should never be able to refund more money than was originally paid. A payment reference is unique: No two transactions should ever share the same payment reference key. An order cannot be fulfilled twice: Once an order status is marked as fulfilled, duplicate webhooks or retried requests must never trigger a second shipment or credit. 5. Handling Discrepancies: Reconciliation Even with great code, things can occasionally get out of sync—maybe a webhook was missed during a server restart, or a network call timed out.
When there is a discrepancy between your internal records and the payment provider's records, reconciliation occurs.
Reconciliation is the process of comparing records between systems and resolving discrepancies. Running regular reconciliation checks (like comparing daily provider reports against your internal database) ensures that any missing or mismatched payments get caught and fixed automatically.
Final Thoughts Working with payment gateways comes down to respecting the nature of distributed systems:
NB: Protect your database with atomicity and clear financial invariants. When you build with these principles in mind, your payment code stays clean, safe, and maintainable for the long run.
feel free to leave your comments
Comments (0)
No comments yet. Be the first to share your thoughts.