llm-driven-system-design / paypal
CLAUDE.md60 starsChanged 10 months agoMITView on GitHub
A simplified P2P payment platform: users hold a wallet, deposit/withdraw funds, send money to each other, and request money with a pay/decline/cancel workflow. The core hard problem is moving money between two wallets correctly under concurrency — a transfer must debit exactly one wallet and credit exactly one other, atomically, and never double-execute on a retry or deadlock when two people pay each other simultaneously. Learning goals: double-entry bookkeeping, optimistic locking vs. SELECT ... FOR UPDATE for wallet balances, deadlock-free…
We split this CLAUDE.md into the rules it gives an agent
3 rules. About 0 are common, 2 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 32
All amounts are integer cents (BIGINT) with DB CHECK constraints preventing negative balances.
- Distinctive: line 57
Should wallet balance be cached in Redis with a short TTL for the read-heavy dashboard, and how do we invalidate it the instant a transfer …
- Distinctive: line 59
Should money requests expire?
≈ “Common” is an estimate: at least 1 in 5 of the 25,680 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
PayPal — Development with Claude
Project Context
A simplified P2P payment platform: users hold a wallet, deposit/withdraw funds, send money to each other, and request money with a pay/decline/cancel workflow. The core hard problem is moving money between two wallets correctly under concurrency — a transfer must debit exactly one wallet and credit exactly one other, atomically, and never double-execute on a retry or deadlock when two people pay each other simultaneously.
Learning goals: double-entry bookkeeping, optimistic locking vs. SELECT ... FOR UPDATE for wallet balances, deadlock-free multi-row locking, and idempotency that survives a crash.
Architecture at a Glance (what actually runs)
Two datastores in docker-compose.yml — this is deliberately a single-relational-DB design; money correctness lives in Postgres, not spread across systems:
| Store | Role | Why this one |
PostgreSQL (pg) | Wallets, transactions, ledger_entries, transfer_requests, payment_methods, and idempotency keys | The transfer + its ledger entries + its idempotency key all commit in one ACID transaction — that atomicity is the whole point |
Valkey/Redis (ioredis) | Session store (connect-redis) + rate-limit counters (rate-limit-redis) | Ephemeral, revocable session state and atomic counters — data whose loss on restart is tolerable, unlike ledger data |
Idempotency keys live in Postgres, not Redis — a deliberate inversion of the usual "cache it in Redis" instinct (see Decision 3). Backend: Express app (app.ts/index.ts) with routes auth, wallet, transfers, requests, paymentMethods, users; services walletService, transferService, idempotencyService, plus circuitBreaker (Opossum), metrics (prom-client), logger (pino). Frontend: React 19 + TanStack Router + Zustand.
Key Design Decisions
1. Double-entry bookkeeping as the reconciliation invariant
Every balance change writes paired debit/credit rows in ledger_entries inside the same transaction that mutates the wallet. The transactions row is the business event; the ledger rows are the accounting impact. The invariant that makes the system auditable: SUM(credits) − SUM(debits) for a wallet must equal wallet.balance_cents. Trade-off given up: 2× write amplification per transfer versus a bare UPDATE balance — accepted because a single mutable balance can't be reconciled or audited after a bug.
2. Optimistic locking for single-wallet ops, ordered FOR UPDATE for transfers
Deposits/withdrawals use a version column (WHERE version = $expected) so a concurrent write is detected and retried rather than silently lost. But a P2P transfer touches two wallets, and two users paying each other at once is a classic deadlock: A locks A→tries B while B locks B→tries A. transferService avoids it by acquiring SELECT ... FOR UPDATE on both wallets in a consistent order (by user_id), so the two transactions can't form a cycle. Trade-off: FOR UPDATE serializes concurrent transfers touching the same wallet — correct over fast, which is the right call for money.
3. Idempotency keys in Postgres, in the payment's own transaction
idempotencyService reads/writes idempotency_keys through the same PoolClient as the transfer. If the transaction rolls back, the key rolls back with it — so it's impossible to have a stored key without a completed payment, or a completed payment without a key. Redis would be sub-ms faster, but a crash between "payment committed" and "Redis SET" would leave the next retry free to re-execute the charge. For money, that consistency gap outweighs the latency. (This is the opposite choice from the merchant-facing payment-system project, which caches in Redis with a DB UNIQUE backstop — worth contrasting.)
4. BIGINT cents everywhere
All amounts are integer cents (BIGINT) with DB CHECK constraints preventing negative balances. No floating-point money in application code, ever. Trade-off: callers must format cents→dollars at the edge, but that's cheap insurance against rounding drift.
5. Session auth over JWT
HTTP-only session cookies (express-session + connect-redis, bcryptjs password hashing) instead of JWT. Rationale: XSS can't read an HttpOnly cookie, and deleting the Redis session revokes access instantly — a JWT would need short expiry (bad UX) or a blacklist (which re-implements sessions anyway). For a financial app, instant revocation wins over statelessness.
Current State
Implemented end to end: registration/login with session auth, wallet deposit/withdraw/balance, P2P transfers with double-entry ledger + optimistic locking + ordered FOR UPDATE, money-request lifecycle (create/pay/decline/cancel with authorization checks), payment-method CRUD with default management, debounced user search, idempotency (Postgres, 24h TTL, background cleanup), Opossum circuit breaker on external calls, express-rate-limit backed by Redis, Prometheus metrics, and pino structured logging. Frontend: dashboard (wallet card, pending requests, recent activity), send/request flows, activity page with type filters.
Intentionally omitted: multi-currency / FX, transfer-request expiry, dispute/refund flows, real bank-transfer or card-processor integration (deposits/withdrawals are simulated), and a WebSocket notification service (request pay/decline is poll/refresh, not push). The production architecture diagram shows Kafka + a notification service — those are the production layer, not built locally.
Iteration & Repair Log
- 2026-07 (CLAUDE.md restructure): This file was already substantive but was organized around a "Development Phases" narrative and lacked an Architecture-at-a-Glance table, an accurate Current State, and a repair log. Restructured to the exemplar shape while keeping the (correct) key design decisions.
- Idempotency location: early instinct was Redis for speed; changed to Postgres-in-transaction after reasoning about the crash-consistency gap (Decision 3).
architecture.mddocuments the same reasoning (§"Idempotency Keys in PostgreSQL (Not Redis)"). - Repo-wide fixes that touched this project: ESM
connect-redisv7 named-import +pino-httpnamed-import hardening; DB/Redis connection-string fallbacks to the docker-compose creds (paypal:paypal123,redis://localhost:6379); the schema-apply path isdb/migrate.tsrun vianpm run db:migrate(also mounted atdocker-entrypoint-initdb.d). - 2026-07-31 — working, but thin. Unlike most of the projects in this sweep, nothing was broken: login, the dashboard, wallet balance, pending requests and payment methods all rendered real seeded data. Two gaps closed:
- The Activity page's Deposits and Withdrawals filters were permanently empty — the seed only ever created two
transferrows, so two of the four filter tabs had nothing to show. The seed now covers all three transaction types plus apendingwithdrawal, with wallet balances reconciled to the ledger's finalbalance_after_centsso the double-entry invariant actually holds against the seeded data. Also added the terminal request states (paid,declined) so the money-request lifecycle is visible rather than just its pending step. init.sqlindexes weren't idempotent (relation "idx_transactions_sender" already existson the second migrate) — the schema is applied by both the initdb mount andnpm run db:migrate. All 7CREATE INDEXstatements now useIF NOT EXISTS; the tables already did.
- The Activity page's Deposits and Withdrawals filters were permanently empty — the seed only ever created two
- Screenshots: 6 → 8, adding the Deposits and Withdrawals filter views.
- Answer docs: backend 344→354 and fullstack 324→360, both of which were under the 350-line floor. Added the asymmetry that deposits/withdrawals are single-sided in the ledger while transfers are paired — which is why the reconciliation invariant is written per wallet, not per transaction — plus a deep dive on why the two-wallet transfer path uses ordered
FOR UPDATErather than the optimistic locking used everywhere else. - CI: the repo-wide smoke-test workflow was removed (no Docker services in CI).
Open Questions
- Should wallet balance be cached in Redis with a short TTL for the read-heavy dashboard, and how do we invalidate it the instant a transfer commits?
- How should multi-currency transfers work — a per-currency wallet with an FX service, or a single base currency with conversion at send time?
- Should money requests expire? An unbounded pending request is a UX and reconciliation smell.
- Dispute/refund: refunds are just reverse ledger entries, but who authorizes them and how do we prevent a refund from being replayed?
SELECT ... FOR UPDATEserializes transfers on a hot wallet — at what throughput does a popular payee need a different model (e.g. append-only ledger with async balance projection)?
Resources
In this repository94 other files it gives its agents.
- AGENTS.mdAGENTS.md
- 20forms-20designs/CLAUDE.mdCLAUDE.md
- ad-click-aggregator/CLAUDE.mdCLAUDE.md
- ai-code-assistant/CLAUDE.mdCLAUDE.md
- airbnb/CLAUDE.mdCLAUDE.md
- airtag/CLAUDE.mdCLAUDE.md
- amazon/CLAUDE.mdCLAUDE.md
- apns/CLAUDE.mdCLAUDE.md
- apple-maps/CLAUDE.mdCLAUDE.md
- apple-music/CLAUDE.mdCLAUDE.md
- apple-pay/CLAUDE.mdCLAUDE.md
- apple-tv/CLAUDE.mdCLAUDE.md
- app-store/CLAUDE.mdCLAUDE.md
- bitly/CLAUDE.mdCLAUDE.md
- calendly/CLAUDE.mdCLAUDE.md
- CLAUDE.mdCLAUDE.md
- coinbase/CLAUDE.mdCLAUDE.md
- collaborative-editor/CLAUDE.mdCLAUDE.md
- confluence/CLAUDE.mdCLAUDE.md
- dashboarding/CLAUDE.mdCLAUDE.md
- discord/CLAUDE.mdCLAUDE.md
- distributed-cache/CLAUDE.mdCLAUDE.md
- docusign/CLAUDE.mdCLAUDE.md
- doordash/CLAUDE.mdCLAUDE.md
- dropbox/CLAUDE.mdCLAUDE.md
- etsy/CLAUDE.mdCLAUDE.md
- excalidraw/CLAUDE.mdCLAUDE.md
- facetime/CLAUDE.mdCLAUDE.md
- fb-live-comments/CLAUDE.mdCLAUDE.md
- fb-news-feed/CLAUDE.mdCLAUDE.md
- fb-post-search/CLAUDE.mdCLAUDE.md
- figma/CLAUDE.mdCLAUDE.md
- gallery/CLAUDE.mdCLAUDE.md
- github/CLAUDE.mdCLAUDE.md
- gmail/CLAUDE.mdCLAUDE.md
- google-calendar/CLAUDE.mdCLAUDE.md
- google-docs/CLAUDE.mdCLAUDE.md
- google-search/CLAUDE.mdCLAUDE.md
- google-sheets/CLAUDE.mdCLAUDE.md
- health-data-pipeline/CLAUDE.mdCLAUDE.md
- hotel-booking/CLAUDE.mdCLAUDE.md
- icloud/CLAUDE.mdCLAUDE.md
- imessage/CLAUDE.mdCLAUDE.md
- instagram/CLAUDE.mdCLAUDE.md
- jira/CLAUDE.mdCLAUDE.md
- job-scheduler/CLAUDE.mdCLAUDE.md
- kindle-highlights/CLAUDE.mdCLAUDE.md
- leetcode/CLAUDE.mdCLAUDE.md
- linkedin/CLAUDE.mdCLAUDE.md
- local-delivery/CLAUDE.mdCLAUDE.md
- loom/CLAUDE.mdCLAUDE.md
- mcplator/CLAUDE.mdCLAUDE.md
- mdreader/CLAUDE.mdCLAUDE.md
- microsoft-teams/CLAUDE.mdCLAUDE.md
- netflix/CLAUDE.mdCLAUDE.md
- news-aggregator/CLAUDE.mdCLAUDE.md
- notification-system/CLAUDE.mdCLAUDE.md
- notion/CLAUDE.mdCLAUDE.md
- online-auction/CLAUDE.mdCLAUDE.md
- payment-system/CLAUDE.mdCLAUDE.md
- The first 60 shown.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
Reviews can't be read right now.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

