Interview Question: What Really Happens When You Click “Pay Now”?
The user clicks the “Pay Now” button.
What actually happens behind the scenes?
If it’s your first time working on a payment system, you might imagine the process is perfectly simple and linear:
- The user clicks Pay.
- The money is deducted.
- A
Payment Successmessage is returned.
But the real world is definitely not that simple. Let’s ask a fundamental question:
Do I, as the site owner, handle the credit card data myself?
The answer is almost always: No.
Handling raw credit card data requires rigorous security compliance. That’s why we use specialized companies for payments, like:
- Stripe
- Paymob
- PayPal
Or others. We call these Payment Gateways. These companies are responsible for dealing with the banks, payment networks, and top-tier security.
So, how do I, as a Developer, communicate with them?
The Answer is API Keys
You will typically find two types of keys:
- Public Key: This is safe to be exposed in the Frontend.
- Secret Key: This is strictly forbidden from leaving the server.
Why? Because this key represents your actual account with the payment provider. If anyone finds it out, they can execute operations and move money in your name.
💬 Naima’s Note: Pythonista here! A quick reminder from the trenches: never, ever hardcode your secret keys in your source code. Use
.envfiles and ensure they are added to your.gitignore. I’ve seen too many repositories accidentally leak Stripe secrets, resulting in massive headaches. Use libraries likepython-dotenvto keep things safe and clean!
The Initial Flow: The User Clicks “Pay”
The first thing our server does isn’t just blindly asking for money. It verifies that:
- The order actually exists.
- The price is correct.
- The product is available.
- The requested amount is exactly what needs to be paid.
After that, the server talks to the Payment Gateway (like Stripe). Here, the Payment Gateway usually returns something called a Payment Session.
This is a temporary session representing the current payment operation so the user can enter their details and pay. It contains information like:
- The Amount
- The Currency
- The Order ID
- Session Expiration Time
- A unique Session ID
Then, the user enters their card details, or pays using Apple Pay, Google Pay, or any other method.
Here is a Sequence Diagram illustrating this initial flow:
sequenceDiagram
participant U as User
participant F as Frontend
participant B as Backend
participant G as Payment Gateway (Stripe)
U->>F: Clicks "Pay Now"
F->>B: Request Checkout (Order ID)
B->>B: Validate Order, Stock & Price
B->>G: Create Payment Session
G-->>B: Return Session ID & URL
B-->>F: Return Session details
F->>U: Redirect to secure payment page
U->>G: Enters Card Details
The Illusion of the Synchronous Response
Beautiful… but here is a very important question:
If the Payment Gateway returns an Error to me, does that mean the money wasn’t deducted?
The answer is No.
And if it returns Success, does that mean the money has actually arrived safely?
Again, not necessarily.
Why? Because there is a massive second party involved in this story: The Bank.
At any moment, you could experience:
- Timeout
- Network Failure
- Connection Lost
Imagine this: The server sent the payment request. The bank deducted the money. But right before the result could return to you, the internet dropped. Now, you are looking at an Error. But the customer’s money was actually deducted!
This introduces complex state management. Here is a State Diagram showing the true lifecycle of an order’s payment status:
stateDiagram-v2
[*] --> Pending
Pending --> Processing : Checkout Initiated
Processing --> Paid : Webhook Confirms Success
Processing --> Failed : Webhook Confirms Failure
Failed --> Processing : User Retries
Paid --> Refunded : Admin Action
Refunded --> [*]
Paid --> [*]
The Source of Truth: Webhooks
We need something that definitively tells me if the operation actually completed or not. Here appears a very important concept called a Webhook.
A Webhook is essentially a message sent from the Payment Gateway to your server. It’s like the gateway is telling you: “Forget about what happened during the initial Request. I will tell you the final result.”
- If the payment succeeded -> It sends a
SuccessEvent. - If it failed -> It sends a
FailureEvent. - If a refund happened -> It sends a
RefundEvent.
In reality, the Webhook is just an Endpoint on your Backend. For example: POST /api/payments/webhook.
When any update happens to the payment process, the Payment Gateway sends a Request to this Endpoint to notify you of the final outcome.
To handle all this asynchronous data reliably, your database needs to track intents and events separately from the core order. Here is an Entity Relationship Diagram (ERD):
erDiagram
ORDER ||--o{ PAYMENT_INTENT : "processes"
PAYMENT_INTENT ||--o{ WEBHOOK_EVENT : "triggers"
ORDER {
int id PK
float total_amount
string status
}
PAYMENT_INTENT {
string provider_id PK
int order_id FK
string idempotency_key
string status
}
WEBHOOK_EVENT {
int id PK
string intent_id FK
string event_type
boolean is_processed
}
Trust, But Verify: The Signature
But here comes a very critical question…
What prevents anyone on the internet from sending a Request to that same Endpoint and telling me the payment succeeded? If that happens, someone could activate orders, subscriptions, or top up their balance without actually paying anything!
Because of this, most payment providers send something called a Signature or Webhook Secret along with the Webhook.
Your server must verify this signature before believing the message. Meaning, you don’t just trust that the Request reached your Endpoint. You must ensure it truly came from Stripe, Paymob, or whichever provider you use. If the Signature is incorrect, you must reject the Request immediately.
This is a crucial security step, because any mistake here could allow an attacker to forge payment notifications and trick your system.
Here is a Flowchart demonstrating this verification logic:
graph TD
A[Receive Webhook POST Request] --> B{Is Signature Valid?}
B -- No --> C[400 Bad Request / Reject Immediately]
B -- Yes --> D{Check Event Type}
D -- payment.success --> E[Mark Order as PAID in DB]
D -- payment.failed --> F[Mark Order as FAILED in DB]
E --> G[Return 200 OK]
F --> G
💬 Naima’s Note: When implementing this verification, return a
200 OKas quickly as possible to the gateway so it doesn’t assume a timeout and keep retrying. Process the heavy database updates in a background task (like Celery or a worker queue) if you can!
The Double-Click Dilemma: Idempotency
Another question…
What if the user clicks “Pay Now” twice? Or the internet was slow so they smashed the button 3 times? Or the app automatically performed a Retry?
Should we create 3 separate payment operations? Of course not.
Here appears a concept called Idempotency.
This means: The exact same request is executed only once, no matter how many times it is repeated.
If the user tries to send the exact same payment operation more than once, the system simply returns the state of the old, existing operation instead of creating new ones and risking deducting the money multiple times. This is one of the most important concepts in any respectable Payment System.
Conclusion: Thinking Like an Engineer
Let’s look at a Class Diagram summarizing a clean, decoupled backend architecture that handles all of these responsibilities:
classDiagram
class PaymentGateway {
<<interface>>
+createSession(orderId, amount, currency)
+verifyWebhookSignature(payload, signature)
}
class StripeProvider {
-secretKey
-webhookSecret
+createSession()
+verifyWebhookSignature()
}
class OrderService {
+validateOrder(orderId)
+updateOrderStatus(orderId, status)
}
class WebhookHandler {
+processEvent(eventData)
}
PaymentGateway <|-- StripeProvider
OrderService <-- WebhookHandler : "updates status"
WebhookHandler --> PaymentGateway : "verifies signature via"
As you can see, payment systems aren’t just an API that gets called. It’s a collection of problems that must be solved:
- Who has the Secret Key?
- How do I protect the payment data?
- How do I know the money was actually deducted?
- How do I deal with Network Issues?
- How do I prevent duplicate payments?
- How do I make sure the message is truly coming from the payment provider?
When you start asking these questions, that’s when you’ve started thinking like an engineer, not just a programmer sending a Request.