A serverless small business loan lending platform built on AWS Lambda, DynamoDB, and SQS FIFO — modeled after real fintech infrastructure (Kapitus-inspired). Built to demonstrate production-grade serverless architecture, NoSQL data modeling, and compliance-aware system design.
Live API: https://fintech.raynercodes.dev/health
Interactive Docs: https://fintech.raynercodes.dev/docs
Author: Leonardo Rayner — GitHub · LinkedIn
A full loan application lifecycle — submission, credit-based decisioning, human review, disbursement, and repayment tracking — built entirely on serverless AWS infrastructure. The goal was to build something that reflects how a real fintech company would architect this: encrypted, audited, idempotent, cost-aware, and deployed through a real CI/CD pipeline with human approval gates.
Client
↓
API Gateway (Regional) — Lambda Authorizer (JWT validation)
↓
Lambda (FastAPI + Mangum)
↓
SQS FIFO Queue ──→ Lambda Worker ──→ DynamoDB (on-demand)
│
↓
S3 Compliance Vault
(Object Lock, 3yr retention)
Compute: AWS Lambda (Python 3.12, FastAPI + Mangum) Database: DynamoDB on-demand — composite sort key, 2 GSIs, zero table scans Queue: SQS FIFO — two-layer duplicate prevention (SQS dedup + DynamoDB conditional writes) plus an automatic content-hash idempotency layer at the API level Auth: Custom JWT + Lambda Authorizer + progressive brute-force lockout Encryption: AES-256-GCM on PII, PBKDF2-HMAC-SHA256 password hashing (600,000 iterations + pepper), separate customer-managed KMS key per secret category Compliance: S3 Object Lock (COMPLIANCE mode, 3-year retention) — tamper-proof audit trail of every loan lifecycle event IaC: AWS SAM + CloudFormation — single source of truth CI/CD: GitHub Actions (CI) → CodePipeline (CD) with two manual approval gates before production
Every loan application includes a credit score, evaluated automatically by the worker Lambda:
| Score | Outcome |
|---|---|
| ≥ 650 | Auto-approved |
| 500–649 | Routed to human review |
| < 500 | Auto-rejected |
A full lifecycle state machine (VALID_TRANSITIONS) governs what a loan is allowed to become next — pending → review/approved/rejected → funded → repaid/defaulted. Terminal states (rejected, repaid, defaulted) are permanently locked; a rejected loan can never be un-rejected.
- Blast-radius minimization: six separate customer-managed KMS keys (JWT secret, encryption key, password pepper, DynamoDB at-rest, S3 audit vault, plus two reserved for planned future integrations — see Roadmap)
- Application-level encryption: AES-256-GCM on sensitive PII, independent of DynamoDB's own at-rest encryption
- Password security: PBKDF2-HMAC-SHA256, 600,000 iterations, per-password salt + a separate pepper held in Secrets Manager
- Lambda Authorizer: validates JWTs before the main API function ever invokes, with progressive brute-force lockout (15min → 30min → 1hr → 24hr)
- Least-privilege IAM: every Lambda role scoped to only the specific KMS keys, tables, and buckets it actually needs — no wildcard resource grants
Every meaningful loan lifecycle event (loan_submitted, credit_score_pulled, status_changed, loan_disbursed) is written to a dedicated S3 bucket with Object Lock in COMPLIANCE mode — once written, a record cannot be deleted or altered by anyone, including account root, for 3 years. Records transition to Glacier Flexible Retrieval after 60 days for cost efficiency without weakening the retention guarantee.
Rather than relying solely on SQS FIFO's 5-minute deduplication window (which fails to catch a resubmission arriving 5 minutes and 1 second later), the API automatically fingerprints each loan application's actual content and rejects duplicates within a 90-second window — with zero client cooperation required, matching how the caller population for this API (Swagger UI testers, scripts, future integrations) can't reliably be expected to supply idempotency keys themselves.
- Daily stale-transaction sweep: flags any loan stuck in
pendingfor 24+ hours (worker normally resolves this in seconds) for human investigation — never deletes financial records, only flags them - Daily DynamoDB backups: on-demand backups of both critical tables, rolling 35-day retention, pruned automatically
Auth — public, no token required
| Method | Path | Description |
|---|---|---|
| POST | /auth/register |
Create a new user account |
| POST | /auth/login |
Authenticate, receive a 15-minute JWT |
| POST | /auth/demo |
Get a JWT for the self-healing demo account — no registration needed |
Loans — all require a Bearer JWT
| Method | Path | Description |
|---|---|---|
| POST | /loans/ |
Submit a loan application (credit score included); auto-decisioned async via SQS + worker |
| GET | /loans/{loan_id} |
Fetch a single loan by its transaction ID |
| GET | /loans/account/{account_id} |
All loans for one account |
| GET | /loans/customer/{customer_id} |
All loans across every account belonging to a customer |
| PATCH | /loans/{loan_id}/status |
Move a loan through its lifecycle (e.g. review → approved → funded) — enforced by the state machine |
Health — public
| Method | Path | Description |
|---|---|---|
| GET | /health |
Service status, version, and stack metadata |
Full request/response schemas, examples, and try-it-out testing are available at /docs.
| Layer | Technology |
|---|---|
| Compute | AWS Lambda, Python 3.12, FastAPI, Mangum |
| Database | DynamoDB (on-demand), 2 GSIs |
| Queue | SQS FIFO |
| Auth | Custom JWT, Lambda Authorizer |
| Encryption | AES-256-GCM, PBKDF2-HMAC-SHA256, AWS KMS |
| Storage | S3 (Object Lock, Glacier lifecycle) |
| IaC | AWS SAM, CloudFormation |
| CI/CD | GitHub Actions, AWS CodePipeline, CodeBuild |
| Testing | pytest, moto |
| Observability | CloudWatch (structured logs, 14-day retention), X-Ray |
60+ tests covering loan lifecycle transitions, credit decisioning boundaries, idempotency/duplicate detection, auth flows, the Lambda Authorizer, and scheduled job logic — using moto to simulate AWS services rather than hitting real infrastructure.
pip install -r requirements-dev.txt
pytest tests/ -vLocal dev deploys directly for fast iteration:
cp -r src ./package/
sam build --template infrastructure/template.yaml --no-cached
sam deploy --parameter-overrides Environment=devStaging and production deploy through a GitHub Actions pipeline (.github/workflows/deploy.yml): test → build → a visible CloudFormation changeset plan → a gated manual approval (via GitHub Environments with required reviewers) → apply. Every promotion to staging or production requires an explicit human approval before anything executes — the plan is generated and printed first, so the reviewer sees the exact diff before approving. Authentication to AWS uses OIDC federation; no long-lived AWS credentials are stored in GitHub.
Being upfront about what's still moving:
- Custom domain (
fintech.raynercodes.dev) via CloudFront — ACM certificate validation and CloudFront distribution creation are isolated behind anEnableCloudFronttoggle in the template, currently disabled. AWS's account verification review for CloudFront is still open (support case filed, no published SLA) — but this is deliberately decoupled from the rest of production, so the API itself runs fully live and stable in the meantime. Flipping this on is a small, isolated deploy once the review clears. - 1,000-concurrent-user load test with Locust — locustfile is built and validated at small scale; the full run is blocked on a Lambda concurrent-execution quota increase (default account limit is well below what's needed; quota increase requested, still pending)
- Original plan called for AWS CodePipeline/CodeBuild for CD orchestration; pivoted to a GitHub Actions-native pipeline after CodeBuild's account-level build-queue quota was denied (see Non-Implemented section) — the GitHub Actions version is fully built, tested, and has completed real promotions through staging and production
These are real AWS account-level reviews with no published SLA — not application bugs. Support cases are open and being tracked.
- AWS CodePipeline / CodeBuild — originally built for CI/CD orchestration; CodeBuild's account-level build-queue quota was denied by AWS support ("continue building account usage history"). Pivoted to a GitHub Actions-native pipeline instead, using GitHub Environments with required reviewers for manual approval gates and OIDC federation for AWS auth — a legitimate, widely-used alternative pattern that sidesteps the account limitation entirely rather than waiting on it
- AWS Cognito — skipped in favor of a custom JWT implementation, to demonstrate deeper auth mechanics
- ElastiCache / Redis — skipped; a 3-tier cache (CloudFront edge → Lambda execution context → DynamoDB cache table) covers the same need at zero idle cost
- API Gateway stage caching — skipped; billed hourly regardless of traffic, conflicts with serverless pay-per-use design
- Distinct reviewer/underwriter role — audit trail currently records the requester's own identity as the "actor" on all events; a production system would require a separate authenticated reviewer role, distinct from the applicant
db-credentials/api-keysKMS keys and secrets — provisioned ahead of need, reserved for a future RDS migration and third-party integrations (Stripe, credit bureau) respectively; not yet wired into any code path
Personal portfolio project. Not licensed for production use by third parties.