Core Entities
PaymentIntent: intent_id (idempotency key), amount_cents, currency, customer_id, payment_method_id, status (CREATED, PROCESSING, SUCCEEDED, FAILED, CANCELLED), created_at, metadata. PaymentMethod: method_id, customer_id, type (CARD, BANK, WALLET), last_four, card_brand, billing_address, is_default, tokenized_card_id (from card vault). Refund: refund_id, payment_intent_id, amount_cents, reason, status (PENDING, SUCCEEDED, FAILED), created_at. FraudSignal: signal_id, payment_intent_id, signal_type, score, action (ALLOW, REVIEW, BLOCK).
Card Processing Flow
1. Customer submits card details -> client-side tokenization (Stripe.js / Braintree SDK) — raw card data never touches your server. 2. Your server receives the token and creates a PaymentIntent. 3. Submit to card network via acquirer: Authorization request (Reserve funds on card). 4. Card network routes to issuer. 5. Issuer approves/declines (based on funds, fraud rules). 6. Authorization response: approved with authorization_code, or declined with reason_code. 7. On approval: Capture (move funds from card to merchant). Authorization and capture can be separate (authorize-capture flow for pre-authorization) or combined (charge immediately). 8. Settlement: acquirer batches and settles with card network daily.
Idempotency
Payment operations must be idempotent — a client retry (network timeout) should not double-charge. Implementation: the client generates an idempotency_key (UUID) for each payment attempt. Server: SELECT * FROM payment_intents WHERE idempotency_key = X. If found: return the existing result (same response as the original). If not found: process and store with the key. The SELECT + INSERT must be atomic: use INSERT … ON CONFLICT (idempotency_key) DO NOTHING and check if the row was inserted. Idempotency keys expire after 24 hours. Same key, different amount: reject (key collision with conflicting parameters — return error).
Refund Processing
class RefundService:
def create_refund(self, intent_id: str, amount_cents: int,
reason: str) -> Refund:
intent = self.db.get_intent(intent_id)
if intent.status != PaymentStatus.SUCCEEDED:
raise ValueError("Can only refund succeeded payments")
already_refunded = self.db.sum_refunds(intent_id)
if already_refunded + amount_cents > intent.amount_cents:
raise ValueError("Refund exceeds original charge")
refund = Refund(intent_id=intent_id, amount_cents=amount_cents,
reason=reason, status=RefundStatus.PENDING)
self.db.insert(refund)
result = self.acquirer.refund(
intent.acquirer_transaction_id, amount_cents
)
status = (RefundStatus.SUCCEEDED
if result.success else RefundStatus.FAILED)
self.db.update_refund(refund.id, status)
return refund
Fraud Detection
Rule-based signals: velocity checks (more than 3 failed attempts in 10 minutes from same IP/card), high-risk countries, mismatched billing address vs IP geolocation, unusual purchase amount vs customer history. ML signals: model trained on historical fraud data returns a fraud score [0, 1]. Actions based on score: score 0.7: decline. Card velocity: track failed authorization attempts per card_number per hour in Redis (INCR card:{hashed_number}:failures:hour EXPIRE 3600). Decline the card if failures > 3 in any hour window.
Card Vault
Never store raw card numbers (PAN) in your database — this requires PCI DSS compliance at the highest level. Use a card vault (Stripe, Braintree, or self-hosted like Basis Theory). The vault stores the raw PAN and returns a token. You store the token. For recurring charges: submit the token to the vault; it retrieves the PAN and processes the charge. This limits your PCI scope dramatically (SAQ A vs SAQ D). Network tokens: Visa and Mastercard issue network tokens that replace the PAN — immune to merchant database breaches since the token is useless without the merchant-specific key.
Webhook Delivery
Payment outcomes (success, failure, dispute) are delivered asynchronously via webhooks. Your system must handle: duplicate webhook deliveries (same event sent twice), out-of-order delivery, and processing failures. Implement an idempotent webhook handler: store processed webhook event_ids; skip duplicates. Use FOR UPDATE SKIP LOCKED for concurrent webhook processors. Sign webhooks with HMAC (Stripe uses X-Stripe-Signature) and verify signatures before processing. Retry failed webhook processing with exponential backoff.
{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “Why is card tokenization critical for payment systems?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Storing raw card numbers (PAN) makes your system a high-value target for attackers. A breach exposes all stored cards. PCI DSS requires extensive controls (network segmentation, encryption, audit logging) for any system storing raw PANs — scope level SAQ D, the most burdensome. Tokenization: the client-side SDK (Stripe.js, Braintree SDK) captures card details in an iframe hosted by the payment processor. The raw PAN never touches your server. The processor returns a single-use or multi-use token. You store the token. For recurring charges: send the token to the processor; it retrieves the PAN from its vault. Your PCI scope drops to SAQ A — minimal controls. A breach of your token store is harmless — tokens are not usable without the processor.”
}
},
{
“@type”: “Question”,
“name”: “How do payment idempotency keys prevent double charges?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Network timeouts cause clients to retry payment requests. Without idempotency: two requests create two charges. With idempotency keys: the client generates a UUID for each payment intent and sends it as a header (Idempotency-Key: uuid). The server stores (idempotency_key, response) with a unique constraint on idempotency_key. On receipt: SELECT the key. If found: return the stored response immediately without processing. If not found: process and store atomically (INSERT with RETURNING or ON CONFLICT check). The result: the first request processes; subsequent retries return the same result without re-processing. Idempotency keys must be unique per logical operation — reuse across different amounts returns an error (conflicting parameters). TTL: 24 hours is standard.”
}
},
{
“@type”: “Question”,
“name”: “How does 3D Secure work and when should you use it?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “3D Secure (3DS) is an additional authentication layer where the card issuer verifies the cardholder before completing the payment. Flow: merchant initiates payment -> acquirer forwards to card network -> network routes to issuer -> issuer presents a challenge (OTP via SMS, biometric, or frictionless background check). The cardholder completes the challenge -> issuer returns authentication result -> merchant completes the capture. Use 3DS when: fraud score indicates elevated risk (0.2-0.7 range), the transaction amount is high, the billing address does not match the IP geolocation, or for recurring subscription setup (first charge). Downside: additional friction increases cart abandonment by 15-20%. Frictionless 3DS (background risk assessment by issuer without user challenge) reduces this but is not always available.”
}
},
{
“@type”: “Question”,
“name”: “How do you design refund processing to handle partial refunds?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Store refunds as separate records linked to the original PaymentIntent. Each refund has its own status (PENDING, SUCCEEDED, FAILED). Validation before processing: sum all existing refunds for the intent; verify sum + new_refund_amount flag for review. (3) Many cards, one IP: 5+ different cards from the same IP in 1 hour -> IP block. (4) Velocity by BIN (first 6 digits of card): many failed attempts from the same bank -> possible BIN attack. (5) Unusual purchase time: customer normally shops 9am-5pm EST; sudden transaction at 3am UTC -> elevated risk score. Combine rule-based signals with an ML model score for final action (allow, 3DS challenge, decline).”
}
}
]
}
Asked at: Stripe Interview Guide
Asked at: Coinbase Interview Guide
Asked at: Shopify Interview Guide
Asked at: Airbnb Interview Guide