001 Fintech Backend Engineering Case Study

EasySwap

Full-stack fintech platform: KYC verification, crypto wallet creation, fiat payments, 2FA. Entire backend architected and built solo from scratch.

Role
Sole Backend Engineer
Stack
Django · DRF · PostgreSQL
Integrations
Dojah · Quidax · Flutterwave
Status
70% complete, funding paused

Project Overview

EasySwap is a Nigerian fintech platform that enables users to seamlessly swap between fiat currency (NGN) and cryptocurrencies (BTC, ETH, USDT, etc.). The platform provides managed fiat wallets backed by Flutterwave virtual accounts, crypto wallets powered by Quidax sub-accounts, and an instant swap engine — all wrapped in a tiered KYC compliance system with multi-layered security.

I served as the sole backend engineer, responsible for the full system architecture, API design, third-party integrations, infrastructure, and CI/CD pipeline.


The Problem

Nigeria's crypto-to-fiat corridor is fragmented. Users typically juggle multiple platforms — one for crypto custody, another for bank transfers, and ad-hoc P2P channels for conversion. Each hop introduces friction, fees, and trust issues. There was no single, API-first product that unified:

EasySwap needed to solve all four in a single backend, while remaining compliant with Nigerian financial regulations and integrating with providers whose APIs have their own quirks and limitations.


Technology Stack

Layer Technology
Language / Framework Python 3.12, Django 5.1, Django REST Framework 3.15
Database PostgreSQL 16
Cache / Broker Redis (django-redis)
Task Queue Celery 5.5 with Redis broker
Authentication JWT via SimpleJWT + dj-rest-auth (cookie-based + Bearer header)
KYC Provider Dojah (BVN, NIN, Driver's License, Passport, Selfie verification)
Fiat Infrastructure Flutterwave (Permanent Virtual Accounts, bank transfers, account verification)
Crypto Infrastructure Quidax (sub-accounts, multi-chain wallets, instant swaps)
Phone Verification Twilio Verify (SMS OTP)
Email Service Resend (transactional emails)
Object Storage Cloudflare R2 (via django-storages + boto3)
API Documentation drf-spectacular (Swagger UI + ReDoc)
Containerisation Docker + Docker Compose
CI/CD GitHub Actions
Monitoring New Relic APM
Audit Trail django-easy-audit
Code Quality flake8, pre-commit hooks, Commitizen (Conventional Commits)

Architecture

High-Level System Design

┌─────────────────────────────────────────────────────────────────┐
│                        Mobile / Web Client                      │
└──────────────────────────────┬──────────────────────────────────┘
                               │ HTTPS (JWT Bearer / Cookie)
                               ▼
┌─────────────────────────────────────────────────────────────────┐
│                     Django REST Framework                       │
│  ┌─────────────┐  ┌──────────────┐  ┌───────────────────────┐  │
│  │  Users App   │  │  Wallet App   │  │  API Docs (Swagger)  │  │
│  │  - Auth      │  │  - Fiat       │  │  drf-spectacular     │  │
│  │  - Profile   │  │  - Crypto     │  └───────────────────────┘  │
│  │  - KYC       │  │  - Swap       │                             │
│  │  - 2FA       │  │  - PIN        │                             │
│  │  - Devices   │  │  - Txns       │                             │
│  └──────┬───────┘  └──────┬────────┘                             │
│         │                 │                                      │
│  ┌──────▼─────────────────▼────────┐                             │
│  │         Service Layer           │                             │
│  │  twilio_service    pin_service  │                             │
│  │  tier_upgrade      fiat_service │                             │
│  │  crypto_service    swap_service │                             │
│  │  flutterwave_svc   quidax_svc  │                             │
│  └──────┬──────────────────┬───────┘                             │
│         │                  │                                     │
│  ┌──────▼──────┐   ┌──────▼──────┐                               │
│  │  Webhooks   │   │   Signals   │                               │
│  │  - Dojah    │   │  (post_save │                               │
│  │  - Quidax   │   │   hooks)    │                               │
│  │  - Flutterwave│  └─────────────┘                              │
│  └─────────────┘                                                 │
└──────────┬──────────────────┬──────────────┬─────────────────────┘
           │                  │              │
    ┌──────▼──────┐   ┌──────▼──────┐  ┌────▼─────┐
    │ PostgreSQL  │   │    Redis    │  │  Celery  │
    │   (Data)    │   │  (Cache +   │  │ (Async   │
    │             │   │   Broker)   │  │  Tasks)  │
    └─────────────┘   └─────────────┘  └──────────┘

Django App Structure

The project is split into two core Django apps with clear domain boundaries:

The service layer isolates all third-party API interactions (Twilio, Dojah, Flutterwave, Quidax) behind clean interfaces, making provider swaps possible without touching view logic.


Core Engineering Decisions

1. Polymorphic Wallet Architecture

One of the most critical design decisions was how to model wallets. Fiat and crypto wallets share fundamental properties (balances, status, feature flags) but diverge significantly in their specifics (virtual account numbers vs. blockchain addresses, single currency vs. multi-network).

Solution: An abstract BaseWallet model with shared fields (available/ledger/frozen balances, status, tier level, feature flags) that FiatWallet and CryptoWallet inherit from. Transactions use Django's ContentType framework (Generic Foreign Keys) to reference any wallet type through a single WalletTransaction ledger.

# BaseWallet (abstract) provides:
# - Triple balance tracking: available, ledger, frozen
# - Status lifecycle: active → frozen → suspended → closed
# - Feature flags: deposit_enabled, withdrawal_enabled, transfer_enabled
# - can_transact() validation, freeze/unfreeze operations

class FiatWallet(BaseWallet):
    # Flutterwave PVA fields: account_number, bank_name, flw_ref, etc.
    currency = models.CharField(default='NGN')

class CryptoWallet(BaseWallet):
    # Blockchain fields: symbol, network, address, tag_or_memo, sync_status
    # Unique constraint: (user, symbol, network)

This approach gives us a single transaction ledger across all wallet types while keeping wallet-specific logic cleanly separated.

2. Triple-Balance Accounting

Every wallet tracks three balances rather than one:

When a user initiates a withdrawal, funds move from available to frozen before the provider confirms. This prevents double-spending without pessimistic database locks, and the WalletTransaction model captures balance_before and balance_after snapshots for every operation, enabling full auditability.

3. Immutable Audit Trail

Every wallet operation — credits, debits, freezes, PIN changes, virtual account creation — produces a WalletAuditLog entry via Generic Foreign Keys. These logs are append-only and include metadata (IP address, device info, admin notes), creating a forensic-grade trail for compliance and dispute resolution.

4. Idempotent Transaction Processing

The WalletTransaction model includes an idempotency_key field. Webhook handlers from Flutterwave and Quidax use this to guarantee at-most-once processing — critical when payment providers retry webhook delivery on timeout.

5. Multi-Layer Authentication

The authentication system implements defence in depth:

  1. JWT with rotation — 60-minute access tokens, 2-day refresh tokens with automatic rotation and blacklisting after use
  2. Device fingerprinting — Unknown devices trigger an SMS OTP challenge via PendingLoginVerification
  3. Trusted device management — Verified devices are trusted for 7 days (configurable via DEVICE_TRUST_DAYS) with rolling expiration
  4. TOTP-based 2FA — Optional authenticator app support via pyotp with 8 one-time backup codes
  5. Login audit log — Every login attempt (success or failure) is recorded with device, IP, and trust status
# Login flow decision tree:
# 1. Validate credentials → JWT pair
# 2. Check device fingerprint against TrustedDevice table
#    → Known + not expired? Issue tokens immediately
#    → Unknown? Create PendingLoginVerification, send OTP via Twilio
# 3. If 2FA enabled → require TOTP verification via TwoFactorSession
# 4. Log everything to DeviceLoginLog

Tiered KYC System

The platform implements a tiered compliance model mirroring Nigerian regulatory requirements:

Tier Requirements Transaction Limits
Tier 0 Email + phone only View-only / minimal
Tier 1 BVN or NIN verified via Dojah Standard daily/monthly limits
Tier 2 BVN + NIN + Government ID (auto-approved) Elevated limits
Tier 3 All Tier 2 + manual admin review Maximum limits

Verification Flow

  1. User submits a document (BVN number, NIN, driver's license image, passport, selfie)
  2. The KYCDocument is created with status=uploaded
  3. A Celery task dispatches the verification to Dojah's API
  4. Dojah processes and calls our webhook endpoint
  5. The dojah_webhooks.py handler updates the document status and triggers the tier_upgrade_service
  6. For Tier 1 and 2, upgrades are auto-approved if all required documents are verified
  7. Tier 3 requires explicit admin approval via the Django admin panel
  8. Wallet tier levels sync automatically on save — FiatWallet.save() and CryptoWallet.save() both pull kyc_tier from the user's profile

Third-Party Integrations

Flutterwave — Fiat Infrastructure

Purpose: NGN deposits and withdrawals for Nigerian users.

Quidax — Crypto Infrastructure

Purpose: Multi-chain crypto custody, wallet address generation, and instant swaps.

Dojah — Identity Verification

Purpose: KYC document verification (BVN lookup, NIN validation, document OCR, selfie matching).

Twilio — Phone Verification

Purpose: SMS OTP delivery for phone verification, login security challenges, and password reset.

Resend — Transactional Email

Purpose: Email verification codes and PIN reset OTPs.


Wallet PIN Security

All financial operations (withdrawals, swaps) require a 4–6 digit wallet PIN, separate from the account password. The PIN system includes:


Webhook Architecture

The system handles webhooks from three external providers, each with distinct verification mechanisms:

Provider Verification Method Handler Complexity
Flutterwave Secret hash comparison Deposit crediting, transfer status
Quidax HMAC signature verification Deposits, withdrawals, swaps, sync
Dojah Webhook payload validation KYC document status updates

All webhook handlers follow a consistent pattern: 1. Verify the webhook signature/hash 2. Parse and validate the payload 3. Perform idempotency check (has this event already been processed?) 4. Execute the business logic within a database transaction 5. Log the operation to the audit trail 6. Return an appropriate HTTP response to the provider


Asynchronous Processing

Celery handles background operations that shouldn't block API responses:

The Celery configuration uses Redis as both broker and result backend, with a 30-minute task time limit and Africa/Lagos timezone for scheduled tasks.


Data Model Summary

Users App — 9 Models

Model Purpose
User Custom user with UUID PK, email-based auth
Profile KYC tier, verification flags, DiceBear avatar fallback
KYCDocument Document submissions with provider tracking
KYCTierUpgrade Upgrade request audit trail
VerificationAttempt Provider verification attempt log
TrustedDevice Device fingerprint trust with rolling expiration
DeviceLoginLog Immutable login attempt audit log
PendingLoginVerification Temp storage for OTP-pending logins
TwoFactorAuth TOTP secret, backup codes, 2FA state

Wallet App — 7 Models

Model Purpose
BaseWallet Abstract: triple-balance, status, feature flags
FiatWallet NGN wallet with Flutterwave virtual account
CryptoWallet Multi-network crypto wallet with sync status
WalletTransaction Universal ledger with Generic FK to any wallet
WalletPIN Hashed PIN with lockout protection
WalletAuditLog Immutable operation audit trail
WithdrawalFee Configurable per-currency, per-network fees
QuidaxSubAccount 1:1 user-to-Quidax mapping

API Design

The API follows RESTful conventions with versioned endpoints under /api/v1/. Key design choices:

Endpoint Groups

Group Endpoints Description
Auth 12 endpoints Register, login, OTP verify, 2FA, logout
Profile & KYC 8 endpoints Profile CRUD, KYC status, tier upgrades, document management
Phone 5 endpoints Send code, verify, status, update, complete update
Password 3 endpoints Reset request, verify OTP, confirm new password
Crypto Wallet 5 endpoints Create, list, balance, withdraw, transactions
Fiat Wallet 7 endpoints Create, list, balance, withdraw, banks, verify account, transactions
Swap 6 endpoints Temp quote, create quote, refresh, confirm, history, detail
PIN 5 endpoints Set, verify, status, reset request, reset confirm
Security 2 endpoints Trusted devices, login history

CI/CD Pipeline

GitHub Actions runs on every push and pull request to main, master, and develop:

  1. Environment setup — PostgreSQL 16 service container, Python 3.12, pip cache
  2. Dependency installation — Full requirements.txt install
  3. Database migrationpython manage.py migrate against the CI Postgres instance
  4. Conventional commit check — Commitizen validates commit message format

Secrets are managed via GitHub repository secrets for all API keys, database credentials, and service configurations.


Infrastructure

Containerisation

The application is containerised with a multi-stage Dockerfile:

Storage


Key Technical Challenges

1. Webhook Reliability at Scale

Payment webhooks are the heartbeat of the system — a missed webhook means a user doesn't see their deposit. The solution involved: - Idempotency keys on every transaction to handle provider retries safely - Balance snapshot fields (balance_before, balance_after) for reconciliation - Comprehensive logging at every webhook processing step - Generic Foreign Key transactions that work across both wallet types

2. Swap Quotation Timing

Quidax quotations expire in 15 seconds. The UX challenge was keeping the client in sync: - Three-tier quotation system: temp (preview) → binding (15s) → confirmed - Refresh endpoint to extend expiring quotations without re-entering details - PIN verification happens at confirmation time, not quotation time, to avoid wasting the window on auth

3. KYC-Wallet Tier Synchronisation

Wallet tier levels must stay in sync with the user's KYC status without explicit sync calls: - Both FiatWallet.save() and CryptoWallet.save() pull kyc_tier from the user's profile - Django signals on profile changes cascade updates to dependent models - The tier_upgrade_service orchestrates the full upgrade flow, including auto-approval logic for Tier 1-2

4. Device Trust vs. Security

Balancing security (challenge unknown devices) with UX (don't OTP-gate every login): - Device fingerprinting identifies returning devices - Rolling 7-day trust window refreshes on each successful login - Expired device cleanup runs nightly via Celery Beat to prevent table bloat - Login audit log enables security forensics without impacting the user flow


Codebase Metrics

Metric Value
Total Python files 84
Total lines of Python ~15,700
Git commits 94
Users app views 2,286 lines
Wallet app views 962 lines
Serializers (combined) 1,111 lines
Django models 16 (across both apps)
Service layer modules 9 files
Webhook handlers 3 (Dojah, Flutterwave, Quidax)
API endpoints ~53

What I Would Do Differently

  1. Event-driven architecture — Replace direct webhook-to-database writes with an event bus (e.g., Django Channels or a lightweight message queue). This would decouple webhook ingestion from business logic processing and improve resilience.

  2. Database-level balance constraints — Add PostgreSQL CHECK constraints on available_balance >= 0 as a safety net beyond application-level validation.

  3. Structured integration tests — The 71 tests that were written and commented out early in development should have been maintained as the codebase evolved. I would invest more in integration tests that cover the full webhook-to-balance-update pipeline.

  4. Rate limiting middleware — While individual endpoints have some rate limiting (e.g., phone verification), a global rate limiting middleware (e.g., django-ratelimit backed by Redis) would provide better DDoS protection.

  5. Separate read/write database — For the transaction ledger, a read replica would prevent heavy reporting queries from impacting transactional throughput.


Outcome

The backend successfully delivers a unified fintech platform with:

Read the article
Creating custom Django commands →