Skip to content

rafaelRojasVi/ledger-bank-api

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

50 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

LedgerBank API

A Learning Portfolio Project Demonstrating Enterprise Elixir/Phoenix Patterns

Elixir Phoenix PostgreSQL CI License

What I Learned โ€ข Quick Demo โ€ข Architecture โ€ข Quick Start โ€ข API Documentation


๐Ÿ’ก What I Learned

This project started as "build a simple banking API" and evolved into a deep dive on professional Elixir architecture. Here's what makes this portfolio piece unique:

๐Ÿš€ Recent Architectural Improvements

Latest Enhancements (January 2025):

Security & Authentication Hardening

  • โœ… JWT Verification Bypass Fixed - Critical security vulnerability resolved
  • โœ… JWT Configuration Unification - Standardized issuer, audience, and expiry across environments
  • โœ… JWT Secret Standardization - Enforced 64+ character secrets with validation
  • โœ… JWT Config-Driven Approach - Removed hardcoded values, made claims configurable
  • โœ… Password Hashing Decoupling - Removed Mix.env() dependencies for production flexibility

Infrastructure & CI/CD Improvements

  • โœ… Oban Configuration Consolidation - Unified queue management across environments
  • โœ… CI Database Readiness - Hardened Postgres health checks with proper dependencies
  • โœ… OpenTelemetry Optimization - Disabled unnecessary instrumentation overhead
  • โœ… CI Quality Gates - Added format checking and warnings-as-errors enforcement
  • โœ… Docker Compose Health Dependencies - Proper service startup ordering

Performance & Scalability Enhancements

  • โœ… Database Index Audit - Added 20+ optimized indexes for pagination and queries
  • โœ… Policy Combinators - Complex authorization logic with all/1, any/1, negate/1
  • โœ… Controller Helper Macros - Standardized CRUD, pagination, and batch operations
  • โœ… Queryable Extensions - Advanced filtering, sorting, and aggregation capabilities
  • โœ… Cache Standardization - TTL helpers and get_or_put variants for consistent caching

Worker & Background Job Improvements

  • โœ… WorkerBehavior Enhancement - Custom retry logic, pre/post-work hooks, telemetry
  • โœ… OpenTelemetry Resource Alignment - Environment-driven resource attributes
  • โœ… Redis Adapter Consistency - Cleaned up stale references, prepared for distributed caching

Documentation & Developer Experience

  • โœ… JWT Token Tradeoffs Documentation - Comprehensive analysis of security vs performance
  • โœ… Configuration Precedence - Clear documentation of JWT and Oban configuration
  • โœ… Problem Registry Audit - Verified RFC 9457 compliance and error discovery
  • โœ… Documentation Clarity - Marked pseudo-code examples to prevent copy-paste confusion

Key Achievements

Pattern What I Built Why It Matters
๐Ÿ—๏ธ Behaviors for DRY Code WorkerBehavior, ServiceBehavior, CacheAdapter, Queryable Eliminated 280+ lines of boilerplate across workers. Shows I understand abstraction vs premature optimization.
๐ŸŽฏ Error Catalog System 40+ error reasons โ†’ 8 categories โ†’ HTTP codes + retry policies Most APIs have inconsistent errors. Mine has a single source of truth that drives retry logic, telemetry, and responses.
๐Ÿ”’ Security by Design Constant-time auth, JWT rotation, RBAC with policies, audit logging Banking-grade security: timing attack prevention, token revocation, role-based permissions.
๐Ÿงช Test Quality 3,796-line integration test, edge cases (null bytes, timing), performance tests I test like companies should: integration flows, security vulnerabilities, and performance regression.
๐Ÿ”„ Background Jobs Done Right Oban + error-aware retry, priority queues, dead letter queue Workers know which errors to retry (external API failures) vs fail fast (business rules).
๐Ÿ“Š Data Access Patterns Keyset pagination, query behaviors, ETS caching Shows I care about performance at scale beyond CRUD.

Architecture Wins

I learned when to abstract and when not to:

โœ… Abstracted because 4+ schemas needed it:

  • SchemaHelpers โ†’ 220 lines of validation duplication removed
  • Queryable โ†’ Consistent filtering/sorting across all resources
  • WorkerBehavior โ†’ Standard telemetry/logging for all workers
  • CacheAdapter โ†’ Switch ETS โ†’ Redis with zero code changes

โŒ Didn't abstract because it would hurt clarity:

  • Controllers - Each has unique validation/authorization
  • Policies - Domain-specific rules don't generalize well
  • Migrations - Database changes need explicit audit trail

Lesson: Abstraction is a trade-off. I chose clarity first, then DRY where duplication was painful (4+ instances).


๐ŸŽค Interview Talking Points

When asked about this project, I highlight:

"I built a banking API to learn production Elixir patterns. The interesting parts:"

  1. Error Catalog System - Instead of scattered {:error, "some string"} everywhere, I built an error taxonomy with 8 categories that drives HTTP codes, retry policies, and telemetry. This meant one change to add circuit breaking for all external API calls.

  2. Behaviors for Scale - When I noticed 180 lines of identical code in two Oban workers, I created WorkerBehavior. Now adding a new worker is 30 lines vs 200, and all workers get telemetry for free.

  3. Security Depth - I implemented constant-time authentication after reading about timing attacks. It's a 10-line change that prevents email enumeration via response time analysis.

  4. Testing Strategy - My integration test is 3,796 lines that validates entire user flows: register โ†’ login โ†’ create payment โ†’ process payment. I also test edge cases like null byte injection and concurrent balance updates.


๐ŸŒ Live Demo

Interactive API Documentation: https://your-app.onrender.com/api/docs

Health Check: https://your-app.onrender.com/api/health

Try it now:

curl https://your-app.onrender.com/api/health

๐Ÿš€ Quick Demo (Local)

Want to run it locally? Here's a 2-minute setup:

Step 1: Start the Server

# Clone and setup
git clone https://github.com/rafaelRojasVi/ledger-bank-api.git
cd ledger-bank-api

# One-command setup (requires Docker)
./test_setup.sh

# Start server
mix phx.server

Step 2: Explore Interactive API Docs

Visit http://localhost:4000/api/docs to see:

  • ๐Ÿ“– OpenAPI/Swagger UI with "Try it out" buttons
  • ๐Ÿ” JWT authentication built into the UI
  • ๐Ÿ“‹ Request/response examples for every endpoint
  • ๐Ÿงช Test endpoints directly from your browser

Step 3: Try the API

# 1. Health check (no auth required)
curl http://localhost:4000/api/health

# 2. Login (get JWT token)
curl -X POST http://localhost:4000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "password": "password123"
  }'

# Response includes access_token - copy it for next requests

# 3. Get user profile (requires auth)
curl http://localhost:4000/api/auth/me \
  -H "Authorization: Bearer <your_access_token>"

# 4. View user statistics (admin only)
curl http://localhost:4000/api/users/stats \
  -H "Authorization: Bearer <admin_token>"

Default credentials after seeding:


๐Ÿ“– Overview

LedgerBank API is a learning project that implements production-grade financial services patterns in Elixir/Phoenix. It demonstrates clean architecture, sophisticated error handling, security best practices, and background job processingโ€”all the patterns you'd find in a real fintech company.

โš ๏ธ Not for Production Use: This is a portfolio/learning project. It simulates banking operations but does not integrate with real financial institutions or handle actual money.

Why This Project Exists

I built this to answer: "How would I architect a complex Elixir API if I had to do it from scratch?"

The result:

  • ๐Ÿ—๏ธ Clean Architecture - Behaviors, services, policies, and pure functions
  • ๐Ÿ”’ Security First - JWT rotation, constant-time auth, RBAC, audit logs
  • ๐ŸŽฏ Error Excellence - Error catalog with retry policies and circuit breakers
  • ๐Ÿš€ Production Patterns - Docker, CI/CD, health checks, monitoring
  • ๐Ÿ“Š Domain-Driven Design - Financial and accounts contexts with clear boundaries
  • โšก Performance - Keyset pagination, ETS caching, query optimization

๐ŸŒŸ Features

Authentication & Authorization

  • โœ… JWT-based authentication with access and refresh tokens
  • โœ… Token rotation for enhanced security
  • โœ… Role-based access control (User, Admin, Support)
  • โœ… Constant-time authentication to prevent timing attacks
  • โœ… Secure password hashing with Argon2
  • โœ… Role-based password complexity (8 chars for users, 15 for admins)

Financial Operations

  • ๐Ÿ’ฐ Multi-bank integration via OAuth2
  • ๐Ÿ’ณ Account management with balance tracking
  • ๐Ÿ“ˆ Transaction history with advanced filtering
  • ๐Ÿ’ธ Payment processing with business rule validation
  • ๐Ÿ”„ Bank synchronization workers for automated updates
  • ๐Ÿฆ Multi-currency support

Banking Integration

  • ๐Ÿ”— OAuth2 client for external bank APIs
  • ๐Ÿ”„ Token refresh mechanisms
  • ๐Ÿ“Š Transaction sync from external sources
  • ๐Ÿฆ Multi-institution support
  • โšก Real-time balance updates

Data Management

  • ๐Ÿ“„ Offset pagination (traditional page/page_size)
  • ๐Ÿ”– Keyset pagination (cursor-based for better performance)
  • ๐Ÿ” Advanced filtering by status, role, date, amount
  • ๐Ÿ”€ Multi-field sorting with direction control
  • ๐Ÿ“Š User statistics and financial health metrics
  • ๐Ÿ’พ ETS-based caching with TTL support

Error Handling & Resilience

  • ๐ŸŽฏ Canonical error structs across all layers
  • ๐Ÿ“‹ Error categorization (validation, authentication, business rules, etc.)
  • ๐Ÿ”„ Automatic retry logic for transient failures
  • ๐Ÿ”Œ Circuit breaker pattern for external services
  • ๐Ÿ“Š Error telemetry and correlation IDs
  • ๐Ÿฅ Health check endpoints
  • ๐ŸŒ RFC 9457 Problem Details compliance with application/problem+json
  • ๐Ÿ“š Problem Type Registry at /api/problems for error discovery
  • โฑ๏ธ Retry-After headers for retryable errors

Background Processing

  • ๐Ÿ”ง Oban integration for reliable job processing
  • ๐Ÿ’ณ Payment processing workers with priority queues
  • ๐Ÿ”„ Bank sync workers with rate limiting
  • ๐Ÿ”„ Job retry strategies based on error categories
  • ๐Ÿ“Š Dead letter queue handling
  • โฐ Scheduled jobs support

Developer Experience

  • ๐Ÿ“š OpenAPI/Swagger documentation
  • ๐Ÿณ Docker & Docker Compose support
  • ๐Ÿงช Comprehensive test suite (1000+ tests)
  • ๐Ÿ“Š Phoenix LiveDashboard for monitoring
  • ๐Ÿ“ Structured logging with correlation IDs
  • ๐Ÿ” Security audit logging

๐Ÿ—๏ธ Architecture

Request Flow (What Happens When You Call the API)

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  1. HTTP Request โ†’ POST /api/auth/login                     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                   โ”‚
         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
         โ”‚  Router + Plugs   โ”‚  โ† SecurityHeaders, RateLimit, SecurityAudit
         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    Adds correlation ID, checks rate limits
                   โ”‚
         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
         โ”‚   AuthController   โ”‚  โ† InputValidator validates email/password
         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    Thin HTTP layer, delegates immediately
                   โ”‚
         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
         โ”‚   AuthService      โ”‚  โ† Pure business logic, no HTTP concerns
         โ”‚   (login_user)     โ”‚    Uses Token.generate_*, UserService.authenticate_user
         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                   โ”‚
         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
         โ”‚   UserService      โ”‚  โ† authenticate_user: constant-time validation
         โ”‚   (authenticate)   โ”‚    Checks password BEFORE checking user status (security)
         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                   โ”‚
         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
         โ”‚  User Schema       โ”‚  โ† Ecto changeset validation
         โ”‚  + Repo.get_by     โ”‚    Database constraints enforced
         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                   โ”‚
         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
         โ”‚   PostgreSQL       โ”‚  โ† ACID transactions, unique constraints
         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                   โ”‚
         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
         โ”‚ Response: JWT      โ”‚  โ† ErrorAdapter maps Error struct โ†’ HTTP 200/400/401
         โ”‚ + User object      โ”‚    Adds correlation ID to response
         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Key Insight: Each layer has one responsibility and errors bubble up as Error structs, not strings.

Clean Architecture Layers

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                        Web Layer                             โ”‚
โ”‚  Controllers โ€ข Plugs โ€ข Validators โ€ข Adapters โ€ข Router       โ”‚
โ”‚                                                              โ”‚
โ”‚  โœ… HTTP concerns only (status codes, headers, JSON)        โ”‚
โ”‚  โœ… InputValidator converts params โ†’ validated data         โ”‚
โ”‚  โœ… ErrorAdapter converts Error structs โ†’ HTTP responses    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                         โ”‚
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                     Business Layer                           โ”‚
โ”‚                                                              โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”               โ”‚
โ”‚  โ”‚  Accounts Contextโ”‚  โ”‚ Financial Contextโ”‚               โ”‚
โ”‚  โ”‚  โ€ข UserService   โ”‚  โ”‚ โ€ข FinancialServ. โ”‚               โ”‚
โ”‚  โ”‚  โ€ข AuthService   โ”‚  โ”‚ โ€ข PaymentWorker  โ”‚               โ”‚
โ”‚  โ”‚  โ€ข Token         โ”‚  โ”‚ โ€ข BankSyncWorker โ”‚               โ”‚
โ”‚  โ”‚  โ€ข Policy        โ”‚  โ”‚ โ€ข Policy         โ”‚               โ”‚
โ”‚  โ”‚  โ€ข Normalize     โ”‚  โ”‚ โ€ข Normalize      โ”‚               โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜               โ”‚
โ”‚                                                              โ”‚
โ”‚  โœ… Pure business logic, no HTTP knowledge                  โ”‚
โ”‚  โœ… Policy functions return true/false (easily testable)    โ”‚
โ”‚  โœ… Normalize functions are pure (no DB, no side effects)   โ”‚
โ”‚                                                              โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                โ”‚
โ”‚  โ”‚           Core Layer                    โ”‚                โ”‚
โ”‚  โ”‚  โ€ข Error Handling  โ€ข Error Catalog     โ”‚                โ”‚
โ”‚  โ”‚  โ€ข Service Behavior โ€ข Validator        โ”‚                โ”‚
โ”‚  โ”‚  โ€ข Worker Behavior โ€ข Cache Adapter     โ”‚                โ”‚
โ”‚  โ”‚  โ€ข Schema Helpers  โ€ข Queryable         โ”‚                โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                โ”‚
โ”‚                                                              โ”‚
โ”‚  โœ… Shared infrastructure for all contexts                  โ”‚
โ”‚  โœ… Behaviors define contracts (swap implementations)       โ”‚
โ”‚  โœ… Error catalog is single source of truth                 โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                         โ”‚
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                      Data Layer                              โ”‚
โ”‚  Ecto Schemas โ€ข Repo โ€ข Migrations โ€ข PostgreSQL              โ”‚
โ”‚                                                              โ”‚
โ”‚  โœ… Database constraints enforced (CHECK, FOREIGN KEY)      โ”‚
โ”‚  โœ… Changesets validate before hitting database             โ”‚
โ”‚  โœ… Migrations are explicit and auditable                   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Skills Demonstrated

This project showcases practical experience with:

Backend Engineering:

  • โœ… RESTful API design with OpenAPI/Swagger documentation
  • โœ… Authentication & authorization (JWT, RBAC, OAuth2 simulation)
  • โœ… Database design (PostgreSQL, Ecto migrations, indexes, constraints)
  • โœ… Background job processing (Oban, priority queues, retry logic)
  • โœ… Caching strategies (ETS, adapter pattern for Redis-ready scaling)
  • โœ… Error handling (categorization, retry policies, circuit breakers)

Software Architecture:

  • โœ… Clean architecture (web โ†’ business โ†’ data layer separation)
  • โœ… Domain-driven design (bounded contexts: Accounts, Financial)
  • โœ… Behavior-driven development (contracts via Elixir behaviors)
  • โœ… Policy-driven authorization (pure functions, easily testable)
  • โœ… Adapter pattern (swappable cache/bank clients)

Testing & Quality:

  • โœ… Comprehensive test coverage (unit, integration, security, performance)
  • โœ… Test-driven development (TDD) approach
  • โœ… Property-based testing (StreamData)
  • โœ… Mock/stub strategies (Mox, Mimic, Bypass)
  • โœ… CI/CD pipeline (GitHub Actions)

DevOps & Operations:

  • โœ… Containerization (Docker, Docker Compose)
  • โœ… Health monitoring (liveness, readiness probes)
  • โœ… Structured logging with correlation IDs
  • โœ… Telemetry & observability
  • โœ… Database migrations in production

Security:

  • โœ… OWASP best practices (Argon2, CSRF, XSS prevention)
  • โœ… Timing attack prevention
  • โœ… Rate limiting & abuse prevention
  • โœ… Security audit logging
  • โœ… Input sanitization & validation

Key Design Patterns

1. Service Behavior Pattern - DRY Without Macros

All services implement a common behavior for consistent error handling and database operations.

Before (Repeated in every service):

def get_user(id) do
  case Repo.get(User, id) do
    nil -> {:error, ErrorHandler.business_error(:user_not_found, %{id: id})}
    user -> {:ok, user}
  end
end

After (ServiceBehavior provides):

@behaviour ServiceBehavior

def get_user(id) do
  context = build_context(__MODULE__, :get_user, %{user_id: id})
  ServiceBehavior.get_operation(User, id, :user_not_found, context)
end

Result: Every service gets standard error handling, context building, and correlation IDs for free.

2. Error Catalog System - Single Source of Truth

Instead of ad-hoc error handling, all errors go through a catalog that defines category, HTTP code, and retry behavior.

Code Example:

# In ErrorCatalog
def reason_codes do
  %{
    :insufficient_funds => :business_rule,      # โ†’ 422, no retry
    :bank_api_error => :external_dependency,   # โ†’ 503, retry 3x
    :invalid_email_format => :validation       # โ†’ 400, no retry
  }
end

# In services, just use the reason:
{:error, ErrorHandler.business_error(:insufficient_funds, context)}

# The catalog automatically:
# - Maps to HTTP 422
# - Sets retryable: false
# - Adds correlation ID
# - Emits telemetry

The Flow:

Error Reason (:insufficient_funds)
    โ†“
ErrorCatalog.category_for_reason() โ†’ :business_rule
    โ†“
ErrorCatalog.http_status_for_category() โ†’ 422
    โ†“
Error.should_retry?() โ†’ false (business rules don't retry)
    โ†“
WorkerBehavior sees should_retry?() = false โ†’ Dead Letter Queue

Why This Matters: One change to the catalog affects all workers, services, and controllers. Adding circuit breaking took 5 minutes because the categories were already there.

3. Policy-Driven Authorization - Pure Functions

Instead of mixing authorization with business logic, I separated all permission checks into Policy modules.

Benefits:

# Pure, easily testable (no DB, no mocks needed)
def can_update_user?(current_user, target_user, attrs) do
  cond do
    current_user.role == "admin" -> true
    current_user.id == target_user.id -> can_update_self?(attrs)
    current_user.role == "support" -> can_support_update_user?(attrs)
    true -> false
  end
end

# Test with zero setup:
test "users can update their own name but not role" do
  user = %User{id: "123", role: "user"}
  assert Policy.can_update_user?(user, user, %{full_name: "New Name"})
  refute Policy.can_update_user?(user, user, %{role: "admin"})
end

Why: Permission rules change often. Keeping them in pure functions means:

  • โœ… No database needed for tests
  • โœ… Easy to audit (all rules in one file)
  • โœ… Can be shared with frontend for UI authorization

4. Normalization Layer - Data Transformation Isolation

Problem: Mixing data cleaning with business logic makes code hard to test.

Solution: Pure Normalize modules for all contexts.

# Input from HTTP
params = %{"email" => " [email protected]  ", "role" => "AdMiN"}

# Normalize
normalized = Normalize.user_attrs(params)
# => %{"email" => "[email protected]", "role" => "user"}  # Force role to "user" for security

# Now service just does business logic:
UserService.create_user(normalized)

Security Win: Normalize.user_attrs/1 forces role to "user" for public registration. Admin creation uses Normalize.admin_user_attrs/1 which allows role selection but requires admin token.

5. Worker Behavior - 280 Lines of Boilerplate Eliminated

Problem: Each Oban worker had 180+ lines of identical code (timing, logging, telemetry, retry decisions).

Solution: WorkerBehavior handles infrastructure, workers implement perform_work/2 only.

Before (PaymentWorker was 245 lines):

use Oban.Worker

def perform(%Oban.Job{args: args} = job) do
  start_time = System.monotonic_time(:millisecond)
  correlation_id = Error.generate_correlation_id()
  context = %{worker: "PaymentWorker", job_id: job.id, ...}
  
  Logger.info("Worker started", context)
  
  case process_payment(args["payment_id"]) do
    {:ok, result} ->
      duration = System.monotonic_time(:millisecond) - start_time
      Logger.info("Worker completed", ...)
      emit_telemetry(:success, duration, ...)
      :ok
    {:error, error} ->
      Logger.error("Worker failed", ...)
      emit_telemetry(:failure, ...)
      if Error.should_retry?(error), do: {:error, error}, else: :discard
  end
end

defp emit_telemetry(...), do: ...  # 30 more lines
defp log_error(...), do: ...       # 20 more lines
# ... (repeat for BankSyncWorker)

After (PaymentWorker is 80 lines):

use LedgerBankApi.Core.WorkerBehavior,
  queue: :payments,
  max_attempts: 5,
  tags: ["payment"]

def worker_name, do: "PaymentWorker"

# WorkerBehavior handles timing, logging, telemetry, retry decisions
def perform_work(%{"payment_id" => id}, context) do
  # Just business logic:
  with {:ok, payment} <- get_payment(id),
       {:ok, result} <- FinancialService.process_payment(id) do
    {:ok, result}
  end
end

Result: Infrastructure is centralized. Adding NotificationWorker requires ~40 lines, not 200.

Module Organization

lib/
โ”œโ”€โ”€ ledger_bank_api/
โ”‚   โ”œโ”€โ”€ accounts/              # User & auth context
โ”‚   โ”‚   โ”œโ”€โ”€ schemas/           # User, RefreshToken
โ”‚   โ”‚   โ”œโ”€โ”€ user_service.ex    # User business logic
โ”‚   โ”‚   โ”œโ”€โ”€ auth_service.ex    # Authentication logic
โ”‚   โ”‚   โ”œโ”€โ”€ token.ex           # JWT generation/validation
โ”‚   โ”‚   โ”œโ”€โ”€ policy.ex          # Permission rules
โ”‚   โ”‚   โ””โ”€โ”€ normalize.ex       # Data transformation
โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€ financial/             # Banking & payments context
โ”‚   โ”‚   โ”œโ”€โ”€ schemas/           # Bank, Account, Payment, Transaction
โ”‚   โ”‚   โ”œโ”€โ”€ integrations/      # External bank API clients
โ”‚   โ”‚   โ”œโ”€โ”€ workers/           # Background job workers
โ”‚   โ”‚   โ”œโ”€โ”€ financial_service.ex
โ”‚   โ”‚   โ”œโ”€โ”€ policy.ex
โ”‚   โ”‚   โ””โ”€โ”€ normalize.ex
โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€ core/                  # Shared infrastructure
โ”‚   โ”‚   โ”œโ”€โ”€ error.ex           # Canonical error struct
โ”‚   โ”‚   โ”œโ”€โ”€ error_catalog.ex   # Error taxonomy
โ”‚   โ”‚   โ”œโ”€โ”€ error_handler.ex   # Error creation & handling
โ”‚   โ”‚   โ”œโ”€โ”€ service_behavior.ex # Service pattern
โ”‚   โ”‚   โ”œโ”€โ”€ validator.ex       # Core validation logic
โ”‚   โ”‚   โ””โ”€โ”€ cache.ex           # ETS caching
โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€ application.ex         # Supervision tree
โ”‚   โ”œโ”€โ”€ repo.ex                # Database repository
โ”‚   โ””โ”€โ”€ release.ex             # Release tasks
โ”‚
โ””โ”€โ”€ ledger_bank_api_web/
    โ”œโ”€โ”€ controllers/           # HTTP controllers
    โ”œโ”€โ”€ plugs/                 # Authentication, authorization, rate limiting
    โ”œโ”€โ”€ adapters/              # Error adapter for HTTP responses
    โ”œโ”€โ”€ validation/            # Input validation
    โ”œโ”€โ”€ router.ex              # Route definitions
    โ”œโ”€โ”€ endpoint.ex            # HTTP endpoint
    โ”œโ”€โ”€ logger.ex              # Structured logging
    โ””โ”€โ”€ telemetry.ex           # Metrics & monitoring

๐Ÿ› ๏ธ Tech Stack

Core Technologies

  • Elixir 1.18+ - Functional, concurrent language
  • Phoenix 1.7+ - Web framework
  • Ecto 3.11+ - Database wrapper and query generator
  • PostgreSQL 16+ - Primary database

Authentication & Security

  • Joken - JWT token generation and validation with config-driven claims
  • Argon2 - Password hashing (OWASP recommended) with environment-specific configuration
  • CORS - Cross-origin resource sharing
  • Security Headers - CSP, HSTS, X-Frame-Options, etc.
  • Policy Combinators - Complex authorization logic with all/1, any/1, negate/1

Background Jobs

  • Oban 2.17+ - Reliable background job processing
  • Telemetry - Metrics and monitoring
  • Phoenix.PubSub - Distributed messaging

HTTP & Integration

  • Req - Modern HTTP client for bank API integration
  • Finch - HTTP client pool
  • Jason - JSON encoding/decoding
  • Swoosh - Email delivery

Development & Testing

  • ExUnit - Testing framework
  • Mox - Mocking library
  • Mimic - Another mocking option
  • Bypass - HTTP mocking
  • Phoenix LiveDashboard - Real-time monitoring
  • Credo - Code analysis (optional)

DevOps

  • Docker & Docker Compose - Containerization
  • GitHub Actions - CI/CD pipeline

๐Ÿ“š Documentation

Complete documentation is available in the docs/ folder:


๐Ÿš€ Quick Start

Prerequisites

  • Elixir 1.18+ and Erlang/OTP 26+
  • PostgreSQL 16+ (via Docker or local installation)
  • Docker and Docker Compose (recommended)
  • Git

Installation

1. Clone the Repository

git clone https://github.com/rafaelRojasVi/ledger-bank-api.git
cd ledger-bank-api

2. Install Dependencies

mix deps.get

3. Setup Environment Variables

Create a .env file (or export directly):

# Database Configuration
export DB_HOST=localhost
export DB_PORT=5432
export DB_USER=postgres
export DB_PASS=postgres
export DB_NAME=ledger_bank_api_dev

# JWT Secret (minimum 32 characters)
export JWT_SECRET="your-super-secret-jwt-key-at-least-32-chars-long-please"

# Phoenix Secret (generate with: mix phx.gen.secret)
export SECRET_KEY_BASE="your-phoenix-secret-key-base-here"

# External Bank API (optional)
export MONZO_CLIENT_ID="your-monzo-client-id"
export MONZO_CLIENT_SECRET="your-monzo-client-secret"

4. Start PostgreSQL with Docker

docker-compose up -d db

Wait for PostgreSQL to be ready:

# Check if PostgreSQL is running
docker-compose ps

# Or use pg_isready
pg_isready -h localhost -p 5432 -U postgres

5. Setup Database

# Create database
mix ecto.create

# Run migrations
mix ecto.migrate

# Seed sample data
mix run priv/repo/seeds.exs

6. Start the Application

# Interactive mode with Phoenix server
iex -S mix phx.server

# Or non-interactive mode
mix phx.server

The API will be available at http://localhost:4000

Automated Setup (Recommended)

Use the provided setup script for a complete environment:

./test_setup.sh

This script:

  • โœ… Starts Docker containers
  • โœ… Drops and recreates databases
  • โœ… Runs all migrations
  • โœ… Seeds sample data
  • โœ… Clears cache

Verify Installation

# Health check
curl http://localhost:4000/api/health

# Expected response:
# {"status":"ok","timestamp":"2025-10-10T...", "version":"1.0.0","uptime":...}

Sample Credentials

After seeding, you can login with:

Regular User:

Admin User:


๐Ÿ“š API Documentation

Base URL

http://localhost:4000/api

Authentication

Most endpoints require authentication via JWT Bearer token:

Authorization: Bearer <your_jwt_token>

API Endpoints

Authentication

Login
POST /api/auth/login
Content-Type: application/json

{
  "email": "[email protected]",
  "password": "password123!"
}

Response:

{
  "data": {
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "user": {
      "id": "uuid",
      "email": "[email protected]",
      "full_name": "Alice Example",
      "role": "user",
      "status": "ACTIVE"
    }
  },
  "success": true,
  "timestamp": "2025-10-10T12:00:00Z"
}
Refresh Token
POST /api/auth/refresh
Content-Type: application/json

{
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Logout
POST /api/auth/logout
Content-Type: application/json

{
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Get Current User
GET /api/auth/me
Authorization: Bearer <access_token>

User Management

List Users (Admin Only)
GET /api/users?page=1&page_size=20&sort=email:asc&status=ACTIVE
Authorization: Bearer <admin_token>

Query Parameters:

  • page - Page number (default: 1)
  • page_size - Items per page (default: 20, max: 100)
  • sort - Sort field and direction (e.g., email:asc, created_at:desc)
  • status - Filter by status (ACTIVE, SUSPENDED, DELETED)
  • role - Filter by role (user, admin, support)
List Users with Keyset Pagination
GET /api/users/keyset?limit=20&cursor={"inserted_at":"2024-01-01T00:00:00Z","id":"uuid"}
Authorization: Bearer <admin_token>
Get User by ID
GET /api/users/{id}
Authorization: Bearer <admin_token>
Create User (Public Registration)
POST /api/users
Content-Type: application/json

{
  "email": "[email protected]",
  "full_name": "New User",
  "password": "password123!",
  "password_confirmation": "password123!"
}

Note: Role is automatically set to user for public registration.

Create User as Admin
POST /api/users/admin
Authorization: Bearer <admin_token>
Content-Type: application/json

{
  "email": "[email protected]",
  "full_name": "New Admin",
  "password": "admin-password-123!",
  "password_confirmation": "admin-password-123!",
  "role": "admin"
}
Update User
PUT /api/users/{id}
Authorization: Bearer <admin_token>
Content-Type: application/json

{
  "full_name": "Updated Name",
  "status": "ACTIVE"
}
Delete User
DELETE /api/users/{id}
Authorization: Bearer <admin_token>
Get User Statistics
GET /api/users/stats
Authorization: Bearer <admin_token>

Response:

{
  "data": {
    "total_users": 100,
    "active_users": 85,
    "admin_users": 5,
    "suspended_users": 15
  },
  "success": true
}

Profile Management

Get Current Profile
GET /api/profile
Authorization: Bearer <access_token>
Update Profile
PUT /api/profile
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "full_name": "Updated Name"
}
Change Password
PUT /api/profile/password
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "current_password": "oldpassword",
  "new_password": "newpassword123!",
  "password_confirmation": "newpassword123!"
}

Payment Management

Create Payment
POST /api/payments
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "amount": "100.50",
  "direction": "DEBIT",
  "payment_type": "PAYMENT",
  "description": "Coffee shop payment",
  "user_bank_account_id": "uuid"
}

Payment Types:

  • TRANSFER - Bank transfer
  • PAYMENT - General payment
  • DEPOSIT - Deposit to account
  • WITHDRAWAL - Withdrawal from account

Directions:

  • CREDIT - Money coming in
  • DEBIT - Money going out
List Payments
GET /api/payments?page=1&page_size=20&direction=DEBIT&status=PENDING
Authorization: Bearer <access_token>
Get Payment Details
GET /api/payments/{id}
Authorization: Bearer <access_token>
Process Payment
POST /api/payments/{id}/process
Authorization: Bearer <access_token>
Get Payment Status
GET /api/payments/{id}/status
Authorization: Bearer <access_token>
Cancel Payment
DELETE /api/payments/{id}
Authorization: Bearer <access_token>
Validate Payment (Dry Run)
POST /api/payments/validate
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "amount": "100.50",
  "direction": "DEBIT",
  "payment_type": "PAYMENT",
  "description": "Test payment",
  "user_bank_account_id": "uuid"
}

Response:

{
  "data": {
    "valid": true,
    "message": "Payment validation successful",
    "payment": {...},
    "account": {...}
  }
}
Get Payment Statistics
GET /api/payments/stats
Authorization: Bearer <access_token>

Health Checks

Basic Health Check
GET /api/health
Detailed Health Check
GET /api/health/detailed

Error Discovery

List All Problem Types
GET /api/problems

Response:

{
  "data": {
    "problems": [
      {
        "code": "insufficient_funds",
        "type": "https://api.ledgerbank.com/problems/insufficient_funds",
        "status": 422,
        "title": "Insufficient funds for this transaction",
        "category": "business_rule",
        "retryable": false
      }
    ],
    "categories": {
      "validation": 12,
      "business_rule": 8,
      "external_dependency": 5
    }
  },
  "success": true
}
Get Problem Details
GET /api/problems/insufficient_funds

Response:

{
  "data": {
    "code": "insufficient_funds",
    "type": "https://api.ledgerbank.com/problems/insufficient_funds",
    "status": 422,
    "title": "Insufficient funds for this transaction",
    "category": "business_rule",
    "retryable": false,
    "retry_delay_ms": 0,
    "max_retry_attempts": 0,
    "description": "Occurs when attempting to process a payment that exceeds the available account balance",
    "examples": ["Payment amount: $150, Available balance: $100"]
  },
  "success": true
}
List Problems by Category
GET /api/problems/category/business_rule

Response:

{
  "status": "ok",
  "timestamp": "2025-10-10T12:00:00Z",
  "version": "1.0.0",
  "uptime": 123456,
  "checks": {
    "database": "ok",
    "memory": "ok",
    "disk": "ok"
  }
}
Readiness Check (for Load Balancers)
GET /api/health/ready
Liveness Check (for Container Orchestration)
GET /api/health/live

Error Responses

All errors follow RFC 9457 Problem Details format:

{
  "type": "https://api.ledgerbank.com/problems/email_already_exists",
  "title": "Email already exists",
  "status": 409,
  "detail": "A user with this email address already exists",
  "instance": "req_1234567890abcdef",
  "code": "email_already_exists",
  "reason": "email_already_exists",
  "category": "conflict",
  "retryable": false,
  "timestamp": "2025-10-10T12:00:00Z",
  "details": {
    "field": "email"
  }
}

Content-Type: application/problem+json

Retry-After Header: Included for retryable errors (e.g., Retry-After: 1000)

Common Error Codes:

  • 400 - Validation error
  • 401 - Authentication required / Invalid token
  • 403 - Insufficient permissions
  • 404 - Resource not found
  • 409 - Conflict (e.g., duplicate email)
  • 422 - Business rule violation
  • 429 - Rate limit exceeded
  • 500 - Internal server error
  • 503 - Service unavailable

๐Ÿ—„๏ธ Database Schema

Entity Relationship Diagram

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚    Users    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜
       โ”‚
       โ”‚ 1:N
       โ”‚
       โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
       โ”‚                      โ”‚
       โ†“                      โ†“
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚RefreshTokens โ”‚      โ”‚UserBankLogins    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜      โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                               โ”‚
                               โ”‚ 1:N
                               โ”‚
                               โ†“
                      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                      โ”‚UserBankAccounts  โ”‚
                      โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                               โ”‚
                  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                  โ”‚            โ”‚             โ”‚
                  โ”‚ 1:N        โ”‚ 1:N         โ”‚ 1:N
                  โ†“            โ†“             โ†“
          โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
          โ”‚Transactions  โ”‚ โ”‚UserPaymentsโ”‚ โ”‚BankBranches  โ”‚
          โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                                 โ”‚
                                                 โ”‚ N:1
                                                 โ†“
                                         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                                         โ”‚    Banks     โ”‚
                                         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Core Tables

users

Primary table for user authentication and profile data.

Column            Type              Description
--------------------------------------------------------------
id                uuid              Primary key
email             string            Unique email address
full_name         string            User's full name
status            string            ACTIVE | SUSPENDED | DELETED
role              string            user | admin | support
password_hash     string            Argon2 hashed password
active            boolean           Account active flag
verified          boolean           Email verified flag
suspended         boolean           Account suspended flag
deleted           boolean           Soft delete flag
inserted_at       timestamp         Creation timestamp
updated_at        timestamp         Last update timestamp

Indexes:
- UNIQUE: email
- INDEX: status, role, active, verified, suspended, deleted
- INDEX: inserted_at, updated_at

banks

Financial institutions available for integration.

Column              Type              Description
--------------------------------------------------------------
id                  uuid              Primary key
name                string            Bank name (unique)
country             string            2-3 letter country code
code                string            Unique bank code
logo_url            string            Bank logo URL
api_endpoint        string            API endpoint URL
status              string            ACTIVE | INACTIVE
integration_module  string            Elixir module for integration
inserted_at         timestamp         Creation timestamp
updated_at          timestamp         Last update timestamp

Indexes:
- UNIQUE: name, code
- INDEX: country, status

bank_branches

Physical or virtual branches of banks.

Column          Type              Description
--------------------------------------------------------------
id              uuid              Primary key
bank_id         uuid              Foreign key โ†’ banks
name            string            Branch name
iban            string            IBAN code (unique)
country         string            2-3 letter country code
routing_number  string            9-digit routing number
swift_code      string            SWIFT/BIC code (unique)
inserted_at     timestamp         Creation timestamp
updated_at      timestamp         Last update timestamp

Indexes:
- UNIQUE: iban, swift_code
- INDEX: bank_id, country
- FOREIGN KEY: bank_id โ†’ banks.id (ON DELETE CASCADE)

user_bank_logins

OAuth2 credentials for user's bank connections.

Column            Type              Description
--------------------------------------------------------------
id                uuid              Primary key
user_id           uuid              Foreign key โ†’ users
bank_branch_id    uuid              Foreign key โ†’ bank_branches
username          string            Bank username
status            string            ACTIVE | INACTIVE | ERROR
last_sync_at      timestamp         Last synchronization time
sync_frequency    integer           Sync frequency in seconds (300-86400)
access_token      text              OAuth2 access token
refresh_token     text              OAuth2 refresh token
token_expires_at  timestamp         Token expiration time
scope             string            OAuth2 granted scopes
provider_user_id  string            User ID from bank provider
inserted_at       timestamp         Creation timestamp
updated_at        timestamp         Last update timestamp

Indexes:
- UNIQUE: (user_id, bank_branch_id, username)
- INDEX: user_id, bank_branch_id, status
- FOREIGN KEY: user_id โ†’ users.id (ON DELETE CASCADE)
- FOREIGN KEY: bank_branch_id โ†’ bank_branches.id (ON DELETE CASCADE)

user_bank_accounts

User's actual bank accounts at financial institutions.

Column              Type              Description
--------------------------------------------------------------
id                  uuid              Primary key
user_bank_login_id  uuid              Foreign key โ†’ user_bank_logins
user_id             uuid              Foreign key โ†’ users
currency            string            3-letter currency code (e.g., USD, EUR)
account_type        string            CHECKING | SAVINGS | CREDIT | INVESTMENT
balance             decimal(15,2)     Current account balance
last_four           string            Last 4 digits of account number
account_name        string            Custom account name
status              string            ACTIVE | INACTIVE | CLOSED
last_sync_at        timestamp         Last synchronization time
external_account_id string            External account identifier
inserted_at         timestamp         Creation timestamp
updated_at          timestamp         Last update timestamp

Indexes:
- UNIQUE: external_account_id
- INDEX: user_bank_login_id, account_type, status
- FOREIGN KEY: user_bank_login_id โ†’ user_bank_logins.id (ON DELETE CASCADE)
- FOREIGN KEY: user_id โ†’ users.id (ON DELETE CASCADE)

Check Constraints:
- balance >= 0 OR account_type = 'CREDIT'

user_payments

User-initiated payments and transfers.

Column                Type              Description
--------------------------------------------------------------
id                    uuid              Primary key
user_bank_account_id  uuid              Foreign key โ†’ user_bank_accounts
user_id               uuid              Foreign key โ†’ users
amount                decimal(15,2)     Payment amount (must be > 0)
direction             string            CREDIT | DEBIT
description           string            Payment description
payment_type          string            DEPOSIT | WITHDRAWAL | TRANSFER | PAYMENT
status                string            PENDING | PROCESSING | COMPLETED | FAILED | CANCELLED
posted_at             timestamp         When payment was posted
external_transaction_id string          External transaction identifier
inserted_at           timestamp         Creation timestamp
updated_at            timestamp         Last update timestamp

Indexes:
- INDEX: user_bank_account_id, amount, payment_type, status, direction
- INDEX: posted_at, external_transaction_id
- FOREIGN KEY: user_bank_account_id โ†’ user_bank_accounts.id (ON DELETE CASCADE)
- FOREIGN KEY: user_id โ†’ users.id (ON DELETE CASCADE)

Check Constraints:
- amount > 0

transactions

Completed transactions on user accounts.

Column        Type              Description
--------------------------------------------------------------
id            uuid              Primary key
account_id    uuid              Foreign key โ†’ user_bank_accounts
user_id       uuid              Foreign key โ†’ users
description   string            Transaction description
amount        decimal(15,2)     Transaction amount (must be > 0)
direction     string            CREDIT | DEBIT
posted_at     timestamp         When transaction was posted
inserted_at   timestamp         Creation timestamp
updated_at    timestamp         Last update timestamp

Indexes:
- INDEX: account_id, posted_at, amount, direction
- COMPOSITE INDEX: (account_id, posted_at)
- FOREIGN KEY: account_id โ†’ user_bank_accounts.id (ON DELETE CASCADE)
- FOREIGN KEY: user_id โ†’ users.id (ON DELETE CASCADE)

Check Constraints:
- amount > 0

refresh_tokens

JWT refresh tokens for secure token rotation.

Column       Type              Description
--------------------------------------------------------------
id           uuid              Primary key
user_id      uuid              Foreign key โ†’ users
jti          string            JWT token identifier (unique)
expires_at   timestamp         Token expiration time
revoked_at   timestamp         Token revocation time (null if active)
inserted_at  timestamp         Creation timestamp
updated_at   timestamp         Last update timestamp

Indexes:
- UNIQUE: jti
- INDEX: user_id, expires_at, revoked_at
- FOREIGN KEY: user_id โ†’ users.id (ON DELETE CASCADE)

oban_jobs

Background job queue (managed by Oban).

See Oban documentation for schema details.


๐ŸŽฏ Error Handling System

Error Architecture

The application implements a three-layer error handling system:

  1. Error Catalog - Central taxonomy of all errors
  2. Error Struct - Canonical error representation
  3. Error Handler - Error creation and processing

Error Categories

:validation          # Input validation failures โ†’ 400
:not_found          # Resource not found โ†’ 404
:authentication     # Authentication failures โ†’ 401
:authorization      # Authorization failures โ†’ 403
:conflict           # Resource conflicts โ†’ 409
:business_rule      # Business logic violations โ†’ 422
:external_dependency# External service failures โ†’ 503
:system             # Internal system errors โ†’ 500

Error Reason Codes

Validation Errors (400)

:invalid_amount_format
:missing_fields
:invalid_direction
:invalid_email_format
:invalid_password_format
:invalid_uuid_format
:invalid_datetime_format
:invalid_name_format
:invalid_role
:invalid_status
:invalid_payment_type
:invalid_currency_format
:invalid_account_type

Not Found Errors (404)

:user_not_found
:account_not_found
:payment_not_found
:token_not_found
:bank_not_found

Authentication Errors (401)

:invalid_credentials
:invalid_password
:invalid_token
:token_expired
:token_revoked
:invalid_token_type

Authorization Errors (403)

:forbidden
:insufficient_permissions
:unauthorized_access

Conflict Errors (409)

:email_already_exists
:already_processed
:duplicate_transaction

Business Rule Errors (422)

:insufficient_funds
:account_inactive
:daily_limit_exceeded
:amount_exceeds_limit
:negative_amount
:negative_balance
:currency_mismatch
:account_frozen
:account_suspended

External Dependency Errors (503)

:timeout
:service_unavailable
:bank_api_error
:payment_provider_error

System Errors (500)

:internal_server_error
:database_error
:configuration_error

Retry Policy Matrix

Category Retryable Circuit Breaker Max Retries Retry Delay
validation โŒ No โŒ No 0 0ms
not_found โŒ No โŒ No 0 0ms
authentication โŒ No โŒ No 0 0ms
authorization โŒ No โŒ No 0 0ms
conflict โŒ No โŒ No 0 0ms
business_rule โŒ No โŒ No 0 0ms
external_dependency โœ… Yes โœ… Yes 3 1000ms
system โœ… Yes โœ… Yes 2 500ms

Error Response Format

{
  "error": {
    "type": "unprocessable_entity",
    "message": "Insufficient funds for this transaction",
    "code": 422,
    "reason": "insufficient_funds",
    "details": {
      "account_id": "uuid",
      "available": "50.00",
      "requested": "100.00"
    },
    "timestamp": "2025-10-10T12:00:00Z"
  }
}

โš™๏ธ Background Jobs with Oban

Queue Configuration

queues: [
  banking: 3,        # Bank API calls (rate-limited)
  payments: 2,       # Payment processing (critical)
  notifications: 3,  # Email/SMS notifications
  default: 1         # Miscellaneous tasks
]

Workers

PaymentWorker

Processes user payments with comprehensive business rule validation.

Features:

  • โœ… Comprehensive payment validation
  • โœ… Balance updates with transactions
  • โœ… Duplicate detection
  • โœ… Priority queue support
  • โœ… Intelligent retry based on error category
  • โœ… Dead letter queue for non-retryable errors

Usage:

# Schedule payment processing
PaymentWorker.schedule_payment(payment_id)

# Schedule with priority (0-9, 0 = highest)
PaymentWorker.schedule_payment_with_priority(payment_id, 0)

# Schedule with delay
PaymentWorker.schedule_payment_with_delay(payment_id, 60)

BankSyncWorker

Synchronizes bank data from external APIs.

Features:

  • โœ… OAuth2 token refresh
  • โœ… Account balance synchronization
  • โœ… Transaction history fetch
  • โœ… Rate limiting respect
  • โœ… Retry with exponential backoff
  • โœ… Uniqueness constraints (5-minute window)

Usage:

# Schedule bank sync
BankSyncWorker.schedule_sync(login_id)

# Schedule with delay
BankSyncWorker.schedule_sync_with_delay(login_id, 300)

Retry Strategies

Workers automatically determine retry behavior based on error categories:

# Business rule violations โ†’ No retry, mark as failed
:insufficient_funds โ†’ Dead Letter Queue

# External dependency errors โ†’ Retry with backoff
:bank_api_error โ†’ 3 retries with exponential backoff

# System errors โ†’ Retry with shorter delay
:database_error โ†’ 2 retries with 500ms base delay

Monitoring Jobs

# Get job status
PaymentWorker.get_payment_job_status(payment_id)

# Cancel scheduled job
PaymentWorker.cancel_payment_job(payment_id)

Phoenix LiveDashboard

Monitor Oban jobs in real-time at:

http://localhost:4000/dev/dashboard/oban

๐Ÿ” Security Features

Authentication Security

JWT Token Management

  • โœ… Access tokens (15-minute expiry)
  • โœ… Refresh tokens (7-day expiry with rotation)
  • โœ… Token revocation support
  • โœ… JTI (JWT ID) for unique token identification
  • โœ… Token blacklisting via database

Password Security

  • โœ… Argon2 hashing (OWASP recommended)
  • โœ… Role-based complexity:
    • Regular users: minimum 8 characters
    • Admin/Support: minimum 15 characters
  • โœ… Password confirmation required
  • โœ… Current password verification for changes

Constant-Time Authentication

Prevents timing attacks for email enumeration:

# SECURITY: Always performs password hashing
# even for non-existent users
@dummy_password_hash Argon2.hash_pwd_salt("dummy_password")

def authenticate_user(email, password) do
  user = get_user_by_email(email) || nil
  password_hash = if user, do: user.password_hash, else: @dummy_password_hash
  
  # Constant time comparison
  password_valid? = Argon2.verify_pass(password, password_hash)
  
  # Check user existence and status AFTER password verification
  # to maintain constant time regardless of account state
end

Authorization

Role-Based Access Control (RBAC)

Roles:
โ”œโ”€โ”€ user    - Regular users
โ”œโ”€โ”€ support - Customer support agents
โ””โ”€โ”€ admin   - System administrators

Policy-Driven Permissions

Pure functions for permission checks:

# User operations
Policy.can_update_user?(current_user, target_user, attrs)
Policy.can_delete_user?(current_user, target_user)
Policy.can_list_users?(current_user)

# Financial operations
Policy.can_create_payment?(current_user, payment_attrs)
Policy.can_process_payment?(current_user, payment)
Policy.can_view_account?(current_user, account)

Security Headers

Automatically applied to all responses:

X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin
Content-Security-Policy: default-src 'none'; ...
Strict-Transport-Security: max-age=31536000; includeSubDomains (production only)

Rate Limiting

Configurable rate limiting to prevent abuse:

# Default configuration
max_requests: 100 per window
window_size: 60 seconds (1 minute)

# Rate limit headers in responses
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 85
X-RateLimit-Reset: 1633987200

Security Audit Logging

Comprehensive security event tracking:

# Logged security events
- Authentication failures
- Authorization failures  
- Rate limit violations
- Suspicious request patterns
- Security policy violations
- Admin actions

Input Validation

Multi-Layer Validation

  1. Web Layer - InputValidator for HTTP inputs
  2. Core Layer - Core.Validator for data formats
  3. Schema Layer - Ecto changesets for database constraints
  4. Business Layer - Business rule validation

Security Validations

  • โœ… UUID format validation
  • โœ… Email format with null byte rejection
  • โœ… SQL injection prevention via parameterized queries
  • โœ… XSS prevention via output escaping
  • โœ… CSRF protection for session-based auth
  • โœ… Length limits on all string fields

Data Integrity

Database Constraints

-- Check constraints
ALTER TABLE users 
  ADD CONSTRAINT status_check 
  CHECK (status IN ('ACTIVE', 'SUSPENDED', 'DELETED'));

ALTER TABLE user_payments
  ADD CONSTRAINT amount_positive_check
  CHECK (amount > 0);

-- Foreign key cascades
ON DELETE CASCADE  -- Automatic cleanup

Sensitive Data Handling

Sanitization

# Sensitive fields removed from logs
sanitized_fields = [
  :password, :password_hash,
  :access_token, :refresh_token,
  :secret, :private_key, :api_key
]

Secure Storage

  • โœ… Passwords โ†’ Argon2 hashed
  • โœ… OAuth tokens โ†’ Encrypted at rest (recommended)
  • โœ… JWT secrets โ†’ Environment variables only
  • โœ… API keys โ†’ Never committed to git

๐Ÿงช Testing

Test Organization

The test suite is organized by domain and concern:

test/
โ”œโ”€โ”€ ledger_bank_api/
โ”‚   โ”œโ”€โ”€ accounts/                      # ~2,500 lines of tests
โ”‚   โ”‚   โ”œโ”€โ”€ auth_service_test.exs      # Authentication logic
โ”‚   โ”‚   โ”œโ”€โ”€ constant_time_auth_test.exs # Timing attack prevention
โ”‚   โ”‚   โ”œโ”€โ”€ edge_case_test.exs         # Edge cases & error handling
โ”‚   โ”‚   โ”œโ”€โ”€ normalize_test.exs         # Data transformation
โ”‚   โ”‚   โ”œโ”€โ”€ policy_test.exs            # Permission rules
โ”‚   โ”‚   โ”œโ”€โ”€ user_service_test.exs      # User business logic
โ”‚   โ”‚   โ”œโ”€โ”€ user_service_keyset_test.exs # Keyset pagination
โ”‚   โ”‚   โ””โ”€โ”€ user_service_oban_test.exs  # Background jobs
โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€ core/                          # ~700 lines of tests
โ”‚   โ”‚   โ”œโ”€โ”€ cache_test.exs             # Caching logic
โ”‚   โ”‚   โ””โ”€โ”€ error_catalog_financial_test.exs # Error system
โ”‚   โ”‚
โ”‚   โ””โ”€โ”€ financial/                     # ~3,000 lines of tests
โ”‚       โ”œโ”€โ”€ financial_service_test.exs # Financial operations
โ”‚       โ”œโ”€โ”€ financial_service_validation_test.exs
โ”‚       โ”œโ”€โ”€ normalize_test.exs         # Financial data transformation
โ”‚       โ”œโ”€โ”€ payment_business_rules_test.exs # Payment validation
โ”‚       โ”œโ”€โ”€ policy_test.exs            # Financial permissions
โ”‚       โ””โ”€โ”€ workers/
โ”‚           โ”œโ”€โ”€ bank_sync_worker_test.exs
โ”‚           โ”œโ”€โ”€ payment_worker_test.exs
โ”‚           โ””โ”€โ”€ priority_execution_test.exs
โ”‚
โ”œโ”€โ”€ ledger_bank_api_web/               # ~6,000 lines of tests
โ”‚   โ”œโ”€โ”€ controllers/                   # Controller integration tests
โ”‚   โ”‚   โ”œโ”€โ”€ auth_controller_test.exs
โ”‚   โ”‚   โ”œโ”€โ”€ payments_controller_test.exs
โ”‚   โ”‚   โ”œโ”€โ”€ users_controller_test.exs
โ”‚   โ”‚   โ”œโ”€โ”€ profile_controller_test.exs
โ”‚   โ”‚   โ”œโ”€โ”€ authorization_test.exs
โ”‚   โ”‚   โ”œโ”€โ”€ security_user_creation_test.exs
โ”‚   โ”‚   โ””โ”€โ”€ users_controller_keyset_test.exs
โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€ plugs/                         # Plug tests
โ”‚   โ”‚   โ”œโ”€โ”€ rate_limit_test.exs
โ”‚   โ”‚   โ””โ”€โ”€ security_headers_test.exs
โ”‚   โ”‚
โ”‚   โ””โ”€โ”€ validation/                    # Input validation tests
โ”‚       โ”œโ”€โ”€ input_validator_test.exs
โ”‚       โ””โ”€โ”€ input_validator_financial_test.exs
โ”‚
โ””โ”€โ”€ support/                           # Test helpers & fixtures
    โ”œโ”€โ”€ conn_case.ex                   # Controller test setup
    โ”œโ”€โ”€ data_case.ex                   # Database test setup
    โ”œโ”€โ”€ oban_case.ex                   # Oban test setup
    โ”œโ”€โ”€ password_helper.ex             # Test password utilities
    โ”œโ”€โ”€ fixtures/
    โ”‚   โ”œโ”€โ”€ users_fixtures.ex
    โ”‚   โ””โ”€โ”€ banking_fixtures.ex
    โ””โ”€โ”€ mocks/
        โ””โ”€โ”€ financial_service_mock.ex

Running Tests

# Run all tests
mix test

# Run tests with coverage
mix test --cover

# Run specific test file
mix test test/ledger_bank_api/accounts/user_service_test.exs

# Run specific test
mix test test/ledger_bank_api/accounts/user_service_test.exs:42

# Run tests by pattern
mix test --only auth

# Run tests with warnings as errors (CI mode)
mix test --warnings-as-errors

# Run tests in parallel
mix test --max-cases 4

Test Categories

Unit Tests

  • โœ… Service business logic
  • โœ… Policy functions
  • โœ… Normalization functions
  • โœ… Validation functions
  • โœ… Error handling

Integration Tests

  • โœ… Controller endpoints
  • โœ… Database operations
  • โœ… Authentication flow
  • โœ… Authorization checks
  • โœ… Background jobs

Security Tests

  • โœ… Constant-time authentication
  • โœ… Role-based access control
  • โœ… Token validation
  • โœ… Rate limiting
  • โœ… Input validation

Test Helpers

Fixtures

# Create test users
user = UsersFixtures.user_fixture()
admin = UsersFixtures.admin_fixture()

# Create banking data
bank = BankingFixtures.bank_fixture()
account = BankingFixtures.bank_account_fixture(user)

Mocking

# Mock financial service
Mimic.copy(LedgerBankApi.Financial.FinancialService)

Mimic.stub(FinancialService, :process_payment, fn id ->
  {:ok, %{id: id, status: "COMPLETED"}}
end)

Continuous Integration

GitHub Actions workflow (.github/workflows/ci.yml):

- โœ… Elixir 1.18.4 / OTP 26.2
- โœ… PostgreSQL 16 via Docker
- โœ… Dependency caching
- โœ… Database creation & migrations
- โœ… Test suite execution with --warnings-as-errors
- โœ… Docker image build

๐Ÿš€ Deployment

Production Checklist

Before deploying to production:

  • Set strong JWT_SECRET (minimum 64 characters)
  • Set secure SECRET_KEY_BASE (generate with mix phx.gen.secret)
  • Configure DATABASE_URL with SSL
  • Set appropriate POOL_SIZE (default: 10)
  • Configure external bank API credentials
  • Set MIX_ENV=prod
  • Set PHX_SERVER=true for standalone deployment
  • Configure logging level (:info or :warning)
  • Set up database backups
  • Configure SSL/TLS certificates
  • Set up monitoring and alerting
  • Configure rate limiting appropriately
  • Set up log aggregation

Environment Variables (Production)

# Application
MIX_ENV=prod
PHX_SERVER=true
PHX_HOST=yourdomain.com
PORT=4000
SECRET_KEY_BASE=<generate-with-mix-phx-gen-secret>

# Database
DATABASE_URL=ecto://user:password@host:5432/database?ssl=true
POOL_SIZE=10

# JWT
JWT_SECRET=<at-least-64-character-secret-key>

# External APIs
MONZO_CLIENT_ID=your-production-client-id
MONZO_CLIENT_SECRET=your-production-client-secret
MONZO_API_URL=https://api.monzo.com

# Oban Queue Configuration (optional)
OBAN_QUEUES=banking:3,payments:2,notifications:3,default:1

Docker Production Deployment

1. Build the Image

# Build production image
docker compose build --pull

# Or manually
docker build -t ledger-bank-api:latest .

2. Run with Docker Compose

# Start all services
docker compose up -d

# View logs
docker compose logs -f app

# Check health
curl http://localhost:4000/api/health/ready

3. Run Migrations

# Inside the container
docker compose exec app bin/ledger_bank_api eval "LedgerBankApi.Release.migrate()"

# Or with docker-compose entrypoint (automatically runs migrations)

Release Build

Build an Elixir release for production:

# Install dependencies
mix deps.get --only prod

# Compile assets and code
MIX_ENV=prod mix compile

# Build release
MIX_ENV=prod mix release

# The release will be in _build/prod/rel/ledger_bank_api/

Run the release:

# Start the server
_build/prod/rel/ledger_bank_api/bin/ledger_bank_api start

# Run migrations
_build/prod/rel/ledger_bank_api/bin/ledger_bank_api eval "LedgerBankApi.Release.migrate()"

# Stop the server
_build/prod/rel/ledger_bank_api/bin/ledger_bank_api stop

Kubernetes Deployment

Example Kubernetes manifests:

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ledger-bank-api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: ledger-bank-api
  template:
    metadata:
      labels:
        app: ledger-bank-api
    spec:
      containers:
      - name: app
        image: ledger-bank-api:latest
   ports:
        - containerPort: 4000
        env:
        - name: PHX_SERVER
          value: "true"
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: db-credentials
              key: url
        - name: SECRET_KEY_BASE
          valueFrom:
            secretKeyRef:
              name: app-secrets
              key: secret_key_base
        - name: JWT_SECRET
          valueFrom:
            secretKeyRef:
              name: app-secrets
              key: jwt_secret
        livenessProbe:
          httpGet:
            path: /api/health/live
            port: 4000
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /api/health/ready
            port: 4000
          initialDelaySeconds: 10
          periodSeconds: 5

---
# service.yaml
apiVersion: v1
kind: Service
metadata:
  name: ledger-bank-api
spec:
  type: LoadBalancer
  ports:
  - port: 80
    targetPort: 4000
  selector:
    app: ledger-bank-api

Database Migrations

Always run migrations before deploying new code:

# Using release task
bin/ledger_bank_api eval "LedgerBankApi.Release.migrate()"

# Or using mix (development/staging)
mix ecto.migrate

Health Monitoring

Set up health check monitoring:

# Liveness check (container is alive)
curl http://your-domain/api/health/live

# Readiness check (can accept traffic)
curl http://your-domain/api/health/ready

# Detailed health (database, memory, etc.)
curl http://your-domain/api/health/detailed

Monitoring & Observability

Telemetry

Built-in telemetry for:

  • HTTP request duration
  • Database query performance
  • Oban job processing
  • Custom business metrics

Logging

Structured JSON logging for production:

  • Request/response logs with correlation IDs
  • Error tracking with stack traces
  • Security audit logs
  • Business event logs

Recommended Tools

  • APM: AppSignal, New Relic, or Datadog
  • Logging: Papertrail, Loggly, or ELK Stack
  • Monitoring: Prometheus + Grafana
  • Error Tracking: Sentry or Rollbar

๐Ÿ› ๏ธ Development

Development Setup

  1. Install Elixir Version Manager (optional)

    asdf install
  2. Start Development Server

   iex -S mix phx.server
  1. Access Development Tools

Development Commands

# Database
mix ecto.setup           # Create, migrate, and seed
mix ecto.reset           # Drop, recreate, migrate, and seed
mix ecto.migrate         # Run pending migrations
mix ecto.rollback        # Rollback last migration
mix ecto.gen.migration   # Generate new migration

# Code Quality
mix format               # Format code
mix credo               # Static code analysis (if installed)
mix dialyzer            # Type checking (if installed)

# Testing
mix test                 # Run all tests
mix test.watch          # Watch mode (if installed)
mix test --cover        # With coverage report

# Interactive Development
iex -S mix              # Start IEx with project loaded
iex -S mix phx.server   # Start IEx with Phoenix server

# Routes
mix phx.routes          # Show all routes

# Dependencies
mix deps.get            # Fetch dependencies
mix deps.update --all   # Update all dependencies
mix deps.clean --all    # Clean dependencies

Code Style

This project follows standard Elixir conventions:

# Format code automatically
mix format

# Check formatting
mix format --check-formatted

Project Structure Best Practices

  1. Contexts - Domain-driven organization (Accounts, Financial)
  2. Services - Business logic layer with standard behavior
  3. Policies - Pure permission functions
  4. Normalize - Data transformation layer
  5. Schemas - Database entities with Ecto
  6. Controllers - Thin HTTP layer, delegates to services
  7. Plugs - Reusable HTTP middleware
  8. Workers - Background job processing

Adding a New Feature

  1. Create Schema & Migration

    mix ecto.gen.migration create_feature_name
  2. Define Schema

    # lib/ledger_bank_api/context/schemas/feature.ex
    defmodule LedgerBankApi.Context.Schemas.Feature do
      use Ecto.Schema
      # ...
    end
  3. Create Service

    # lib/ledger_bank_api/context/feature_service.ex
    defmodule LedgerBankApi.Context.FeatureService do
      @behaviour LedgerBankApi.Core.ServiceBehavior
      # ...
    end
  4. Add Policy (if needed)

    # In lib/ledger_bank_api/context/policy.ex
    def can_do_feature?(user, resource), do: ...
  5. Create Controller

    # lib/ledger_bank_api_web/controllers/feature_controller.ex
    defmodule LedgerBankApiWeb.FeatureController do
      use LedgerBankApiWeb.Controllers.BaseController
      # ...
    end
  6. Add Routes

    # lib/ledger_bank_api_web/router.ex
    scope "/api/features" do
      pipe_through [:api, :authenticated]
      # ...
    end
  7. Write Tests

    # test/ledger_bank_api/context/feature_service_test.exs
    # test/ledger_bank_api_web/controllers/feature_controller_test.exs

๐Ÿ”ฎ What I Would Build Next

Having mastered these patterns, here's my learning roadmap:

Immediate Next Steps (1-2 weeks)

  1. GraphQL API โ†’ Rebuild this with Absinthe

    • Compare REST vs GraphQL for complex financial queries
    • Learn N+1 query prevention with Dataloader
    • Schema stitching for microservices
  2. Real Banking Integration โ†’ Connect to Plaid API

    • OAuth2 flow for real bank connections
    • Webhook handling for transaction updates
    • Error handling for external API failures
  3. Frontend Dashboard โ†’ React + TypeScript

    • Consume this API with proper JWT handling
    • Real-time updates via Phoenix Channels/WebSockets
    • Charts for financial data visualization

Medium Term (1-2 months)

  1. Event Sourcing โ†’ Rebuild with Commanded/EventStore

    • Learn CQRS pattern for financial audit trails
    • Compare event-driven vs CRUD
    • Time-travel debugging for payments
  2. Distributed Systems โ†’ Multi-node Elixir cluster

    • Replace ETS cache with Redis (my CacheAdapter makes this easy)
    • Learn clustering with libcluster
    • Distributed Oban with Redis queues
  3. Observability โ†’ Add AppSignal or Datadog

    • Custom telemetry events
    • Distributed tracing with correlation IDs
    • Alerting on business metrics (failed payments, auth failures)

Advanced Learning (3-6 months)

  1. Microservices โ†’ Split into Auth, Payments, Accounts services

    • Learn service boundaries and API gateways
    • Distributed transactions / Saga pattern
    • Service mesh with Istio
  2. Machine Learning Integration โ†’ Fraud detection

    • Anomaly detection on payment patterns
    • Real-time scoring with Nx (Elixir ML library)
    • A/B testing for fraud rules
  3. Mobile API โ†’ Add GraphQL subscriptions

    • Real-time balance updates
    • Push notifications for transactions
    • Offline-first mobile patterns

๐Ÿค Contributing

This is a learning/portfolio project, but contributions are welcome if you're learning too!

Getting Started

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Make your changes
  4. Run tests (mix test)
  5. Format code (mix format)
  6. Commit your changes (git commit -m 'Add amazing feature')
  7. Push to the branch (git push origin feature/amazing-feature)
  8. Open a Pull Request

Pull Request Guidelines

  • โœ… Include tests for new features
  • โœ… Update documentation as needed
  • โœ… Follow existing code style
  • โœ… Ensure all tests pass
  • โœ… Keep commits focused and atomic
  • โœ… Write clear commit messages
  • โœ… Update CHANGELOG.md (if applicable)

Code Review Process

  1. All PRs require approval from maintainers
  2. CI must pass (tests, formatting, etc.)
  3. Address review feedback promptly
  4. Keep PR scope focused and reasonable

๐Ÿ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.


๐Ÿ™ Acknowledgments

Built with:

  • Elixir - Functional, concurrent programming language
  • Phoenix Framework - Productive web framework
  • Ecto - Database wrapper and query generator
  • Oban - Robust background job processing
  • Joken - JWT implementation

๐Ÿ“ž Contact & Portfolio

About the Developer

Hi! I'm Rafael, and I built this project to learn production Elixir patterns. If you're:

  • ๐Ÿ‘” Hiring for Elixir/backend roles โ†’ Let's talk about what I learned building this
  • ๐ŸŽ“ Learning Elixir too โ†’ Feel free to ask questions or open issues
  • ๐Ÿ”ง Want to contribute โ†’ PRs welcome! See patterns you'd do differently? Let's discuss!

Connect with me:

Other Projects

Check out my other learning projects:

  • ๐Ÿšง [Your other Elixir project]
  • ๐Ÿšง [Your frontend project]
  • ๐Ÿšง [Your infrastructure project]

๐Ÿ“š Resources & Inspiration

This project was influenced by:

Books:

  • Designing Elixir Systems with OTP by James Edward Gray II & Bruce A. Tate
  • Programming Phoenix 1.4 by Chris McCord, Bruce Tate, & Josรฉ Valim

Blog Posts & Talks:

Open Source Projects:


๐Ÿ“ Developer Notes

Why I Made Certain Choices

Q: Why not use Phoenix.Token instead of Joken?
A: I wanted to learn JWT internals and implement token rotation myself. Phoenix.Token is great, but building with Joken taught me about claims validation, signers, and security considerations.

Q: Why ETS instead of Redis for cache?
A: I implemented CacheAdapter behavior so switching to Redis is one config change. ETS keeps the project simple to run locally, but the architecture is Redis-ready.

Q: Why so many test files?
A: I wanted to learn different testing strategies:

  • Integration tests (full user flows)
  • Security tests (timing attacks, injection)
  • Performance tests (N+1 queries, concurrent updates)
  • Edge case tests (null bytes, boundary conditions)

Q: Is this production-ready?
A: The patterns are production-grade, but you'd need:

  • Real banking integration (Plaid, Stripe)
  • Distributed cache (Redis)
  • Proper secrets management (Vault)
  • APM/monitoring (AppSignal, New Relic)
  • Rate limiting per user (currently per IP)

Made with โค๏ธ using Elixir and Phoenix

Built as a learning project โ€ข Not for production financial transactions

โฌ† Back to Top

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published