A multi-currency payment processor.
FCY Payment Processor is a backend service for multi-currency wallet/account operations and transfer processing. It supports user onboarding, account creation, funding, FX rate conversion, charges/VAT computation, and fund transfers across supported currencies (USD, EUR, GBP, NGN).
Transfers are processed through a transactional posting model with internal transient (suspense/GL) accounts to ensure controlled debit/credit movement, traceability, and settlement handling for both internal and external transfer scenarios.
For solution design artifacts, refer to the docs/ directory for:
- architecture decisions
- ERD
- sequence flow
The testing approach in this project focuses on correctness of financial behavior and transfer safety:
- Business-rule-first tests for core use-case services.
- Explicit negative-path coverage for validation and failure cases.
- Mock-based unit tests around service boundaries to keep logic isolated.
- Incremental, reviewable test additions per feature/task.
- Priority on transfer integrity (debit/credit/charges/vat/settlement behavior) over broad but shallow coverage.
- A TDD-leaning flow would likely reduce implementation iterations and rework, but it increases upfront delivery time, which was a trade-off in this time-sensitive project.
The system implements concurrent goroutines in transfer processing and startup initialization:
Key Performance Metrics:
- Application startup time: Average 180ms (including database migrations) - reduced from initial >250ms
- Transfer latency (with goroutines + optimized DB pool): Average 110ms (lowest: 92ms)
- Transfer latency (sequential, without goroutines, same DB pool config): Average 115ms (lowest: 110ms)
- Performance improvement: ~4.5% faster with optimized goroutines
Initial goroutine implementation increased transfer latency from 125ms to 220ms. Root cause analysis identified the database connection pool as the bottleneck. After tuning the pool configuration:
Optimized pool settings:
- Max idle connections: 20
- Max open connections: 30
- Idle timeout: 5 minutes
- Connection lifetime: 15 minutes
These settings reduced transfer time by 50% (from 220ms back to 110ms) while enabling concurrent goroutine operations.
Observation: Database pool size directly determines goroutine performance. Without adequate connection pooling, goroutines introduce overhead that outweighs concurrency benefits.
For lightweight operations, the marginal improvement from goroutines is modest (4-5%). However, postulating to a distributed queue-driven architecture (e.g., with Kafka):
- Estimated latency: 50-70ms per transaction
- Achieved through parallel worker processing, batch handling, and distributed database connections
- Current synchronous HTTP model becomes the bottleneck at scale; async queue-based intake is the path to sub-100ms latency
- Install Docker + Docker Compose and ensure Docker daemon is running.
- Clone the repo and
cdinto project root. - If you do not want to change any values, launch directly with defaults:
docker compose up --build- If you want custom values, update
docker-compose.ymlfirst (see section below), then launch:
docker compose up --buildThe command starts:
db(PostgreSQL 16)app(API onhttp://localhost:8080)
The app runs migrations on startup and ensures default rates/internal transient accounts exist.
To test /transfer-funds, you need at least a valid sender account number.
Recommended flow:
- Create a user with
POST /create-user.- This returns a
customerId.
- This returns a
- Create account(s) with
POST /create-accountusing thatcustomerIdwith an initial deposit of atleast 1 unit of the currency specified.- Supported currencies are only:
USD,EUR,GBP,NGN. - One customer can have multiple accounts across different currencies, but not duplicate account for the same currency.
- Supported currencies are only:
- Fund the sender account (for example via
POST /deposit-funds). - Call
POST /transfer-funds.
Transfer mode selection:
- Internal transfer:
- Set
beneficiaryBankCodeto100100. - Beneficiary account must be an internal account in this app.
- Set
- External transfer:
- Set
beneficiaryBankCodeto a participant bank code fromGET /get-participant-banks. - External transfers terminate in an external GL account in the DB (not a real beneficiary account in this app).
- Once the external GL is credited and external reference is generated, the system assumes beneficiary value has been delivered via beneficiary bank.
- Set
Edit docker-compose.yml.
Under services.db.environment, set:
POSTGRES_DBPOSTGRES_USERPOSTGRES_PASSWORD
Then under services.app.environment, update DATABASE_DSN to match the same DB values:
DATABASE_DSN=Host=db;Port=5432;Database=<POSTGRES_DB>;Username=<POSTGRES_USER>;Password=<POSTGRES_PASSWORD>;Timeout=30;CommandTimeout=30
Under services.app.environment, change, if you want to use a different Basic Auth credentials:
CHANNEL_IDCHANNEL_KEY
Use values appropriate for the target environment.
If 8080 or 5432 is occupied, change:
services.app.ports(left side host port)services.db.ports(left side host port)
Example:
9000:8080exposes API onhttp://localhost:9000
Under services.app.environment, adjust if you want:
GREY_BANK_CODECHARGE_PERCENT,VAT_PERCENT,CHARGE_MIN_AMOUNT,CHARGE_MAX_AMOUNT- Internal/external GL account numbers
Start in background:
docker compose up --build -dView logs:
docker compose logs -f appStop:
docker compose down