Multi-Tenant Wallet & Payments Infrastructure
From debugging silent payment failures to architecting a reconciliation-first wallet system across five payment providers.
- Period
- 2023 — 2025
- Role
- Backend Engineer — owned design and implementation
- Node.js
- Express.js
- MySQL
- Redis
- Stripe
- Razorpay
- Paystack
- PawaPay
- Peach Payments
Overview
A wallet and payments layer for a multi-tenant mobility SaaS platform, supporting white-labeled deployments across multiple markets. The system handles passenger charges, driver credits, pending-balance settlement, and real-time reconciliation across five payment providers.
The Problem
The work began as a production incident: funds were being deducted from passengers but never credited to drivers. The debit and credit legs of a transaction were not atomic — a failure after the debit left money in limbo, with no reconciliation process to detect or repair the inconsistency.
Fixing the bug required tracing transaction flows through a large, interlinked codebase — and made it clear the platform needed a wallet architecture designed around correctness, not just a patch.
Requirements
- Every transaction must be traceable end-to-end: who was charged, who was credited, and when
- Inconsistencies must be detectable automatically, not discovered via user complaints
- Multi-tenant: each white-labeled client operates in isolation with its own provider configuration
- Extensible to new payment providers without touching core wallet logic
- Support for pending balances and delayed settlement flows
Architecture
The wallet is modeled as a ledger: state transitions are recorded as transaction entries rather than direct balance mutations, so the balance is always derivable and auditable. Provider integrations sit behind a common interface, keeping provider-specific quirks (webhook formats, settlement timing, currency handling) out of core wallet logic.
Real-time reconciliation compares internal ledger state against provider records, surfacing mismatches for automated or manual repair before users notice them.
Trade-offs
- Ledger-style records add write volume and storage versus mutating a single balance row — accepted, because auditability is the point of a payments system
- A common provider interface means the integration layer targets the lowest common denominator; provider-specific optimizations require deliberate escape hatches
- Real-time reconciliation adds operational cost, traded against the far higher cost of silent money loss
Challenges
The hardest part was not writing new code but understanding existing behavior: payment flows crossed multiple repositories and services, and the original incident could only be fixed safely by mapping every path that touched wallet state.
Extending to African-market providers (Paystack, PawaPay, Peach Payments) surfaced assumptions baked in from Stripe and Razorpay — settlement timing, webhook reliability, and currency handling all differed enough to force the abstraction to earn its keep.
Results
- Transaction inconsistencies reduced by 20% after the reconciliation-first redesign
- Payment layer extended from two providers to five without changes to core wallet logic
- White-labeled tenants onboard with provider configuration, not code changes
Lessons Learned
Payments code is only as good as its failure paths. The happy path was never the problem — designing for the partial-failure states (debit succeeded, credit did not) is the actual work.
Reconciliation is not an afterthought or a monthly batch job; treating it as a first-class, real-time system component is what turns 'we think balances are right' into 'we know'.