FinTech Architecture

Subscription Billing System

Handling money at scale: Finite State Machines for invoicing, Idempotent Webhooks, and safe PCI-Compliant payment flows.

Data Model: User vs Customer

Never mix Auth data with Billing data. Keep them loosely coupled.

🔑 User Table (App DB)
  • user_id (UUID)
  • email
  • stripe_customer_id (Foreign Key)
💳 Payment Gateway (Stripe)
  • Customer object
  • PaymentMethods (Tokens)
  • Subscriptions

Subscription Lifecycle (State Machine)

Billing logic is complex. Model it as a Finite State Machine (FSM) to handle transitions deterministically.

Trialing
→
Active (Paid)

⬇ Payment Fails
Past Due
→
Canceled
OR
Unpaid
  • Active: Payment successful. Grant feature access.
  • Past Due: Payment failed (insufficient funds). Enter Dunning (retry logic). Do NOT revoke access yet.
  • Canceled/Unpaid: Retries exhausted (e.g., after 14 days). Revoke access.

Idempotency & Webhooks

Payment gateways send webhooks (e.g., invoice.paid) asynchronously. Network issues can cause duplicate events. Your handler MUST be Idempotent.

Rule: Never process the same payment event twice. It could result in double provisioning or false analytics.
# Python (Flask) Example: Handling Stripe Webhooks safely
import stripe
from flask import Flask, request, jsonify

@app.route('/webhook', methods=['POST'])
def stripe_webhook():
    payload = request.data
    sig_header = request.headers['Stripe-Signature']
    event = None

    try:
        # 1. Verify Signature (Security)
        event = stripe.Webhook.construct_event(payload, sig_header, endpoint_secret)
    except ValueError:
        return 'Invalid payload', 400

    # 2. Idempotency Check (Redis/DB)
    event_id = event['id']
    if redis.exists(f"processed_event:{event_id}"):
        return jsonify({'status': 'ignored - duplicate'}), 200

    # 3. Process Event based on Type
    if event['type'] == 'invoice.payment_succeeded':
        handle_payment_success(event['data']['object'])
    
    # 4. Mark as Processed (TTL 24h)
    redis.set(f"processed_event:{event_id}", "true", ex=86400)
    
    return jsonify({'status': 'success'}), 200

PCI Compliance (SAQ A)

Never let raw Credit Card numbers touch your servers. If they hit your API logs, you are liable.

Concept Best Practice
Tokenization Frontend sends Card Data directly to Stripe (Stripe.js). Stripe returns a safe token (e.g., `tok_123`). Your backend only saves the token.
Stripe Elements Using hosted inputs in `iframe` ensures card data never touches your DOM, reducing compliance scope to SAQ A (the easiest level).

Summary

  • State Machines: Essential for managing subscription status (`active`, `past_due`, `canceled`).
  • Dunning: Automation for recovering failed payments (smart retries).
  • Idempotency: Treat every webhook as if it could be delivered 5 times.
  • Compliance: Offload all sensitive data handling to the Payment Processor (Tokenization).