PrepZone Logo
PrepZone

Payment and Fintech Systems

Ledger accounting, idempotent payments, PCI boundaries and reconciliation at fintech scale.

Read these first

Why this matters

  • StreamHub processes creator payouts, subscription billing, and tip transactions — payment bugs mean lost revenue, double charges, or regulatory fines.
  • Idempotency is non-negotiable: network retries must never create duplicate charges.
  • The ledger is append-only; balances are computed, never directly mutated.

Payment system components

  • Payment gateway — Stripe, Adyen, or Razorpay handles card tokenisation and PCI compliance.
  • Ledger service — double-entry accounting; every debit has a matching credit.
  • Idempotency store — Redis or DB keyed by idempotency key; returns cached response on retry.
  • Webhook processor — async confirmation from payment gateway (charge succeeded, payout completed).
  • Reconciliation engine — daily batch compares internal ledger with gateway settlement reports.
  • Payout service — batch creator earnings to bank accounts on a schedule.

Double-entry ledger

Payment platform architecture

CLIENT
Merchant app
SECURITY
AWS WAFmTLS + rate l…
NETWORK
API Gateway
COMPUTE
Payment APIEKS + idempot…
ANALYTICS
Fraud svcSageMaker RT
DATABASE
Ledger RDSCP writes
INTEGRATION
MSK settlem…async stream
WAF → idempotent API → ledger RDS → SageMaker fraud → MSK settlement.

Every transaction creates balanced journal entries — total debits equal total credits.

Java
CREATE TABLE ledger_entries (
  id            UUID PRIMARY KEY,
  journal_id    UUID NOT NULL,
  account_id    UUID NOT NULL,
  amount_cents  BIGINT NOT NULL,  -- positive = credit, negative = debit
  currency      CHAR(3) DEFAULT 'USD',
  description   TEXT,
  created_at    TIMESTAMPTZ DEFAULT now()
);

CREATE TABLE accounts (
  id            UUID PRIMARY KEY,
  owner_id      UUID,
  account_type  VARCHAR(32),  -- 'user_wallet', 'platform_revenue', 'escrow'
  currency      CHAR(3) DEFAULT 'USD'
);
Java
def process_tip(from_user: str, to_creator: str, amount_cents: int, idempotency_key: str):
    journal_id = uuid4()
    entries = [
        {"account_id": user_wallet(from_user), "amount_cents": -amount_cents},
        {"account_id": creator_wallet(to_creator), "amount_cents": amount_cents - platform_fee(amount_cents)},
        {"account_id": platform_revenue(), "amount_cents": platform_fee(amount_cents)},
    ]
    assert sum(e["amount_cents"] for e in entries) == 0
    ledger.append_journal(journal_id, entries)

Idempotent payment API

Java
POST /v1/payments/charge
Idempotency-Key: tip_usr42_crt99_20260401
Content-Type: application/json

{
  "amount_cents": 500,
  "currency": "USD",
  "source": "pm_card_visa_4242",
  "description": "Tip for creator crt_99"
}
Java
def charge(request, idempotency_key: str):
    cached = idempotency_store.get(idempotency_key)
    if cached:
        return cached  # return same response on retry

    result = stripe.PaymentIntent.create(
        amount=request.amount_cents,
        currency=request.currency,
        payment_method=request.source,
        confirm=True,
        idempotency_key=idempotency_key
    )

    ledger.record_charge(result)
    response = format_response(result)
    idempotency_store.set(idempotency_key, response, ttl=86400)
    return response

PCI compliance boundary

Card numbers never touch StreamHub servers. The client tokenises via Stripe.js.

Java
const { paymentMethod } = await stripe.createPaymentMethod({
  type: 'card',
  card: cardElement,
});
// Send paymentMethod.id (pm_xxx) to server — never the card number
AspectPCI scopeApproach
Card data collectionStripe.js / Elements on clientSAQ-A eligibility (simplest)
Server-side chargesPayment method tokens onlyNever store PAN/CVV
WebhooksVerify Stripe signaturePrevent forged payment events
Payout bank detailsStripe Connect onboardingPlatform never sees bank account numbers
AuditImmutable ledger + webhook logRegulatory compliance evidence
  • Card data collection

    PCI scopeStripe.js / Elements on client
    ApproachSAQ-A eligibility (simplest)
  • Server-side charges

    PCI scopePayment method tokens only
    ApproachNever store PAN/CVV
  • Webhooks

    PCI scopeVerify Stripe signature
    ApproachPrevent forged payment events
  • Payout bank details

    PCI scopeStripe Connect onboarding
    ApproachPlatform never sees bank account numbers
  • Audit

    PCI scopeImmutable ledger + webhook log
    ApproachRegulatory compliance evidence

Using Stripe Connect keeps StreamHub at SAQ-A — the lowest PCI compliance burden.

Webhook processing

Payment gateways confirm asynchronously via webhooks.

Java
@app.post("/webhooks/stripe")
def stripe_webhook(request):
    payload = request.body
    sig = request.headers["Stripe-Signature"]
    event = stripe.Webhook.construct_event(payload, sig, WEBHOOK_SECRET)

    if event.type == "payment_intent.succeeded":
        payment = event.data.object
        ledger.confirm_charge(payment.id)
        notify_creator(payment.metadata["creator_id"], payment.amount)

    elif event.type == "payment_intent.payment_failed":
        ledger.reverse_pending_charge(event.data.object.id)
        notify_user_failure(event.data.object.metadata["user_id"])

    return {"status": "ok"}

Process webhooks idempotently — Stripe may deliver the same event multiple times.

Reconciliation

Daily job compares internal ledger with Stripe settlement reports.

Java
def reconcile(date: str):
    internal = ledger.get_settled_transactions(date)
    external = stripe.BalanceTransaction.list(created={"gte": date_start, "lt": date_end})

    mismatches = []
    for txn in internal:
        stripe_txn = external.get(txn.stripe_id)
        if not stripe_txn or stripe_txn.amount != txn.amount_cents:
            mismatches.append(txn)

    if mismatches:
        alert_finance_team(mismatches)
        create_adjustment_entries(mismatches)

Scale estimates

MetricEstimate
Daily transactions2M
Peak TPS~500
Average transaction$4.50
Daily volume$9M
Creator payouts per month50K
Reconciliation mismatches< 0.01%
Idempotency key TTL24 hours
  • Daily transactions

    Estimate2M
  • Peak TPS

    Estimate~500
  • Average transaction

    Estimate$4.50
  • Daily volume

    Estimate$9M
  • Creator payouts per month

    Estimate50K
  • Reconciliation mismatches

    Estimate< 0.01%
  • Idempotency key TTL

    Estimate24 hours

Ledger writes are low-volume relative to reads. Optimise for correctness and auditability, not raw throughput.

Quick recall

Everything you need if you only revisit this box.

  • Double-entry ledger: every debit has a matching credit; balances are computed, never mutated.
  • Idempotency keys prevent duplicate charges on network retries — cache response for 24 hours.
  • PCI boundary: card data stays on Stripe.js client; server handles tokens only.
  • Webhooks confirm payments asynchronously; process idempotently with signature verification.
  • Daily reconciliation catches mismatches between internal ledger and gateway settlements.
  • Store money as integer cents — never floating-point.

Test yourself

Answer these before moving on — recall is what makes it stick.