A full-stack, atomic wallet ledger system designed for high-concurrency financial transactions, complete with a modern, reactive client dashboard.
This project is split into a decoupled frontend and backend to mirror production environments.
- Frontend: A Vite + React application utilizing
Tailwind v4andshadcn/uifor a responsive interface. State and data fetching are managed byTanStack React Queryto ensure real-time UI updates and cache invalidation. - Backend: A Node.js/Express REST API adhering to a strict 3-tier architecture (Routes ➡️ Controllers ➡️ Models) for clear separation of concerns.
- Database: PostgreSQL. The schema relies on an append-only ledger pattern to guarantee financial data integrity.
- Strict Double-Entry Accounting: The database splits financial data into two specialized tables. The
transactionstable acts as the immutable receipt (recording the Sender, Receiver, and Event). Theledgertable handles the actual double-entry math, creating two rows (a debit and a credit) for every event. This guarantees perfect auditability and lightning-fast balance calculations viaSUM(). - Concurrency & Atomicity: Transfers are wrapped in strict
BEGIN/COMMITtransaction blocks. I utilized PostgreSQL'sFOR UPDATErow-level locking on the sender's wallet to prevent race conditions and overdrafts during concurrent requests. - Subjective Metadata via SQL: Transaction types (e.g. 'TRANSFER_IN' vs 'TRANSFER_OUT') and descriptions are not stored in the database. Because a single transfer is viewed differently by the sender and receiver, these fields are dynamically derived on the fly using SQL
CASEstatements during theGET /transactionsrequest. This keeps the database perfectly normalized. - Client-Driven Idempotency: The frontend generates a UUID and passes it via the
Idempotency-Keyheader to thePOST /transferandPOST /depositendpoint. This guarantees that network retries or accidental double-clicks cannot double-charge a user. - The User Resolution Pattern: To respect the backend's strict requirement for UUIDs while providing a consumer-grade UX, I added a
/users/findendpoint. This allows the frontend to search by human-readable names while seamlessly passing the required user ID to the transfer payload.
I prioritize core ledger integrity over expansive feature sets.
- No Automated Test Suite:
- Trade-off: I omitted automated tests to focus on concurrency logic and frontend UI polish.
- Resolution: In production, I would add Integration Tests (Supertest/Vitest) to fire concurrent transfer requests and mathematically prove the row-level locks prevent race conditions, alongside React Testing Library for the UI state.
- Calculated Balances vs. Cached Balances:
- Trade-off: Calculating balance via
SUM()on every request is 100% accurate but becomes a performance bottleneck at enterprise scale. - Resolution: In production, I would use asynchronous materialized views or a Redis cache to serve read-heavy balance requests, keeping the ledger purely for writes.
- Trade-off: Calculating balance via
- Authentication Shortcut:
- Trade-off: I bypassed a complex password/hashing system (bcrypt) in favor of an automatic JWT generation flow upon account creation.
- Resolution: This allowed me to secure endpoints with
Bearertokens and focus engineering time on database concurrency rather than auth boilerplate.
- Frontend Polling vs. WebSockets:
- Trade-off: Used React Query polling (
refetchInterval) to update the receiving user's dashboard, which adds unnecessary database reads. - Resolution: In a real-world app, I would replace this with WebSockets or Server-Sent Events (SSE) to push ledger updates directly to the client.
- Trade-off: Used React Query polling (
- The "System Wallet" for External Deposits: I assumed that external deposits (e.g., via a credit card or Stripe) should not break the fundamental rule of double-entry accounting (where the sum of the entire ledger must always equal zero). I implemented a "System Wallet" (a master account with a zero-UUID). When a user deposits funds, it is recorded as a strict database transfer from this System Wallet, allowing the platform to easily audit the total amount of real-world cash held in the system.
The entire application has been containerized for developer convenience. Ensure Docker Desktop is running.
Prerequisites:
- Git
- Docker Desktop running on your machine.
git clone https://github.com/godstimedev/lance-wallet.git
cd lance-walletWhile in the root directory (where the docker-compose.yml lives), spin up the containers.
Note: Make sure PORT: 3000, 4000, 5432 are not in use on your machine
docker-compose up --build -dNote: The PostgreSQL container uses an initialization script that automatically builds the tables, indexes, and seeds the initial System Wallet. No manual database migrations are required!
Once Docker finishes building, you can access the completely isolated environments:
Frontend Dashboard: http://localhost:3000
Backend API: http://localhost:4000
Database (Postgres): localhost:5432
Processing 10,000,000 transactions a day averages out to ~115 transactions per second (TPS), with peak load potentially hitting 1,000+ TPS. To support this without locking the database, the architecture must transition from synchronous to heavily asynchronous.
-
Compute Infrastructure: Transition the Node.js backend to stateless microservices running on Kubernetes (EKS/GKE) to allow horizontal auto-scaling based on CPU utilization.
-
Implement load testing or stress testing: This involves build into our deployment CI/CD pipeline, load or stress test with predefined amount of users on any major release, replicating our production environment to observe bottlenecks and fix them.
-
Asynchronous Queues: Holding an HTTP connection open for a FOR UPDATE lock will exhaust database connections at high TPS. The POST /transfer endpoint must become asynchronous, returning a 202 Accepted and pushing the payload onto a message broker like Apache Kafka or RabbitMQ. Background workers then pull from this queue and execute the DB transactions sequentially.
-
Database & Caching: Introduce Redis to cache user balances. When a Kafka worker successfully processes a transfer, it updates the Redis cache. The frontend queries Redis (sub-millisecond latency) instead of PostgreSQL.
-
Monitoring & Observability: Implement Prometheus and Grafana for real-time monitoring of message broker queue depth and transaction latency. Set up strict PagerDuty alerting for Dead Letter Queues (DLQ) to catch any transactions that fail their concurrency locks and exceed retry limits.