NexPay — Payment Infrastructure
Payment processing infrastructure with double-entry ledger, idempotent transactions, reconciliation engine, and financial audit trails — built for correctness over convenience.
Domain Knowledge
What problem this project solves
Payment systems are the hardest category of backend engineering because financial correctness is non-negotiable. A social feed bug is an inconvenience; a payment bug is a lawsuit. NexPay treats every monetary operation as a ledger entry rather than a balance update — this way, the full history of every rupee is always traceable. The system handles double-spending protection, idempotent charge creation, automated reconciliation, and audit-ready transaction logs.
Architecture
How the system is structured
The core of NexPay is a double-entry ledger. Every financial operation creates at least two entries: a debit from one account and a credit to another. Balances are derived values, never stored directly — preventing the classic 'balance = 100, balance = 50' bug where history disappears. The payment flow: client request with idempotency key → validation → ledger entry creation → gateway charge → ledger settlement → webhook notification. If any step fails, the entire transaction rolls back atomically using PostgreSQL transactions.
Data Model
Schema design and data flow
The ledger schema uses a journal entry pattern: each transaction has a unique ID, a pair of entries (debit/credit), and references to the affected accounts. Accounts track running balance as a materialized view computed from the ledger, never stored directly. A separate reconciliation table stores daily snapshots of gateway balances vs internal balances for automated comparison.
Key Challenges
Hardest problems encountered
Double-spending prevention was the hardest technical problem. Without protection, two concurrent requests could charge the same ₹100 balance twice, resulting in negative balance. Solved with a combination of: (1) idempotency keys — every charge request carries a unique key, and the system ensures each key processes only once, (2) pessimistic row-level locks on the account during transaction, (3) atomic PostgreSQL updates with CHECK constraints preventing negative balances. Reconciliation was another challenge — matching thousands of daily transactions between Stripe records and internal records requires careful handling of timing differences and partial settlements.
Scaling Strategy
How the system grows
The ledger table is append-only and partitioned by month for query performance. Idempotency keys are stored in Redis with TTL for fast lookups, with PostgreSQL as the source of truth. Reconciliation runs as a daily batch job. Read replicas serve reporting queries without impacting write throughput.
Security
Defense-in-depth approach
PCI compliance is handled by Stripe's tokenization — raw card data never touches the server. API keys are hashed with bcrypt. All financial operations are logged to an append-only audit table. Idempotency keys prevent replay attacks. Rate limiting per merchant prevents abuse. Failed transactions are logged with full context for debugging.
Failure Handling
Resilience and recovery
If a charge succeeds at the gateway but the ledger entry fails, a background reconciler detects the orphan and reverses it. If the gateway itself is unreachable, the request is queued for retry with idempotency. The daily reconciliation job sends alerts for any discrepancy > 0.01 INR.
Observability
Monitoring and debugging
Every financial operation generates an audit trail: who initiated it, when, from which IP, the idempotency key, gateway response, and ledger entries. Dashboards track: success rate, average settlement time, failed payment rate, reconciliation discrepancies, and ledger balance vs gateway balance. Alerts fire on any unreconciled difference.
Trade-offs
Engineering decisions and alternatives
PostgreSQL was chosen over specialized ledger databases (like LedgerDB) to avoid adding another infrastructure dependency — PostgreSQL's transactional guarantees and CHECK constraints are sufficient for this scale. Redis idempotency keys use TTL-based expiration rather than permanent storage, accepting a tiny window where an expired key could cause a duplicate (mitigated by PostgreSQL-level unique constraints).
Architecture Decisions
Key choices and what was rejected
Senior-Level Topics
Concepts this project explores