Vishal.dev
Back
Backend

NexPay — Payment Infrastructure

Payment processing infrastructure with double-entry ledger, idempotent transactions, reconciliation engine, and financial audit trails — built for correctness over convenience.

Node.jsTypeScriptPostgreSQLRedisBullMQStripe APIDocker
100%
Ledger accuracy
<200ms
Charge latency
99.99%
Uptime target
Daily
Reconciliation

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

Decision
Chosen
Rejected
Balance storage
Derived from ledger entries
Stored balance column
Idempotency
Redis + PostgreSQL dual check
Database-only (slower)
Payment gateway
Stripe tokenization
Self-hosted (PCI nightmare)
Reconciliation
Daily batch + alert
Real-time (unnecessary complexity)

Senior-Level Topics

Concepts this project explores

Double-Entry LedgerDistributed TransactionsSagasEvent SourcingIdempotencyReconciliationPCI ComplianceFinancial Auditing