Skip to content

About

An enterprise-grade, event-driven payment backend engine emulating core ledger transactions. Built using Python 3.11, AWS Lambda, Amazon API Gateway, and DynamoDB Local, fully orchestrated via Docker and AWS SAM CLI for reproducible local evaluation without an AWS account.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

Serverless Wallet Transfer Engine

An enterprise-grade, event-driven payment backend engine emulating core ledger transactions. Built using Python 3.11, AWS Lambda, Amazon API Gateway, and DynamoDB Local, fully orchestrated via Docker and AWS SAM CLI for reproducible local evaluation without an AWS account.


Architecture & System Design

                     +----------------------------------------------+
                     |            Client Request                    |
                     |  (Authorization: Bearer <JWT> | SigV4)       |
                     +----------------------------------------------+
                                            |
                                            v
+----------------------------------------------------------------------------------------+
|                               Amazon API Gateway                                       |
|                                                                                        |
|   [POST /admin/topup]                                [GET /me/balance]                 |
|   (Auth: AWS_IAM)                                    [POST /transfers]                 |
|         |                                            (Auth: Lambda Token Authorizer)   |
|         |                                                        |                     |
|         |                                                        v                     |
|         |                                            +-----------------------+         |
|         |                                            |  AuthorizerFunction   |         |
|         |                                            |  - HMAC-SHA256 Verify |         |
|         |                                            |  - Scope Verification |         |
|         |                                            |  - IAM Policy Emit    |         |
|         |                                            +-----------------------+         |
|         |                                                        |                     |
|         |                                                        v Context Injection   |
|         v                                                        v (userId, scopes)    |
|   +-----------------------+                          +-----------------------+         |
|   |  AdminTopupFunction   |                          |  Balance / Transfer   |         |
|   |  (System Credits)     |                          |  Handler Functions    |         |
|   +-----------------------+                          +-----------------------+         |
+----------------------------------------------------------------------------------------+
                                            |
                                            v
+----------------------------------------------------------------------------------------+
|                                   Amazon DynamoDB                                      |
|                                                                                        |
|    TransactWriteItems (ACID 4-Way Transaction)                                         |
|    ---------------------------------------------------------------------------------   |
|    1. IDEMPOTENCY#<key>  -> Condition: attribute_not_exists(pk) [Double-Charge Guard]  |
|    2. USER#<sender>      -> Condition: #st = ACTIVE AND balance >= :amount             |
|    3. USER#<receiver>    -> Condition: attribute_exists(pk) AND #st = ACTIVE           |
|    4. TX#<tx_id>         -> Ledger audit entry record insertion                        |
+----------------------------------------------------------------------------------------+

Core Design Decisions

1. Zero-Trust Identity Propagation

Client bodies are strictly untrusted for identity claims. The sender username is never parsed from the request body on transfer routes. Instead, the Lambda Authorizer validates the JWT signature, unpacks the subject (sub), evaluates route-level scopes (wallet:read, wallet:transfer), and injects the verified identity into requestContext.authorizer.userId. Downstream handlers consume this trusted context exclusively.

2. ACID Concurrency Control via TransactWriteItems

Distributed ledger architectures require strict prevention of double-spends and race conditions. Rather than using distributed locks or relational table deadlocks:

  • Debit, credit, idempotency reservation, and ledger logging are bundled into a single 4-item atomic transaction.
  • If a sender balance drops below the transfer threshold, or if either party is marked INACTIVE/FROZEN, DynamoDB atomically aborts the entire transaction batch with zero partial state.
  • In-flight concurrent requests on the same wallet result in a TransactionCanceledException, mapped to 409 Conflict to safely trigger client retries.

3. Distributed Idempotency Mechanism

Network timeouts between client and gateway risk duplicate charges upon retry.

  • Transfers require an Idempotency-Key header.
  • The transaction reserves IDEMPOTENCY#<key> with a condition check attribute_not_exists(pk).
  • If an operation with the same key was previously processed for that user, the engine short-circuits execution and returns a replay response (200 OK with DUPLICATE_ACCEPTED), protecting against duplicate ledger deductions.

4. Single-Table DynamoDB Schema Design

To maintain $O(1)$ single-digit millisecond latency while supporting diverse access patterns, all entities coexist in a single table named WalletEngine:

Entity Pattern Partition Key (pk) Sort Key (sk) Attributes
User Wallet USER#<username> WALLET balance (Number), status (String), updated_at
Idempotency Lock IDEMPOTENCY#<key> IDEMPOTENCY caller_id (String), status (String), created_at
Ledger Audit Log TX#<key> LOG from_user, to_user, amount, timestamp

5. Separation of Concerns in Route Authorization

  • User APIs (GET /me/balance, POST /transfers): Protected by a custom Lambda Authorizer verifying Bearer JWTs and claims.
  • System APIs (POST /admin/topup): Protected by native AWS_IAM authorization in the CloudFormation definition, ensuring internal back-office maintenance can only be executed by callers with explicit AWS SigV4 STS credentials.

Prerequisites

Before starting, ensure your system has the following running:

  • Docker & Docker Compose (Docker Engine running)
  • AWS SAM CLI (sam --version >= 1.110.0)
  • Python 3.10+ & make

(Note: No AWS account or cloud credentials are required; all AWS services are emulated locally.)


Quickstart (Local Evaluation)

Step 1: Environment Setup & Database Initialization

Run the following in your terminal to spin up the local DynamoDB container and seed test wallets:

# 1. Install local test suite dependencies (requests, pyjwt)
make setup

# 2. Start DynamoDB Local container
make up

# 3. Initialize DynamoDB schema and seed test accounts
make seed

Seeded Test Accounts:

  • alice: Balance: $1000 | Status: ACTIVE
  • bob: Balance: $250 | Status: ACTIVE
  • charlie_inactive: Balance: $500 | Status: INACTIVE (Frozen Account)

Step 2: Build & Start the API Gateway Engine

In Terminal 1, run:

make start

What happens behind the scenes:

  1. Runs sam build --use-container, pulling the official AWS Lambda Linux build image to package handlers and resolve dependencies cleanly.
  2. Mounts the compiled artifacts into an emulated API Gateway server at http://127.0.0.1:3000.
  3. Bridges the Lambda containers to the wallet-net Docker network to enable communication with dynamodb-local:8000.

Step 3: Run the Automated Verification Suite

Open Terminal 2 and execute the automated test suite:

make test

Expected Verification Output:

--- 1. Testing GET /me/balance (Alice) ---
Status: 200, Body: {"userId": "alice", "balance": 1000, "status": "ACTIVE"}

--- 2. Testing Scope Deny (Missing wallet:read) ---
Status: 403, Body: {"message":"User is not authorized to access this resource"}

--- 3. Testing POST /transfers (Alice -> Bob: $100) ---
Status: 201, Body: {"status": "SUCCESS", "from": "alice", "to": "bob", "amount": 100, "idempotencyKey": "tx-uuid-001"}

--- 4. Testing Idempotency (Replaying same key) ---
Status: 200, Body: {"status": "DUPLICATE_ACCEPTED", "message": "Transfer previously completed", "idempotencyKey": "tx-uuid-001"}

--- 5. Testing Inactive Wallet (Alice -> Charlie: $50) ---
Status: 403, Body: {"error": "Wallet for receiver (charlie_inactive) is inactive or frozen"}

--- 6. Testing Admin Topup (Bypasses JWT) ---
Status: 200, Body: {"status": "TOPUP_SUCCESS", "userId": "alice", "newBalance": 1400}

API Reference & Manual Testing

To run manual calls via cURL, Postman, or Thunder Client, first generate a signed development token:

export TOKEN=$(python3 -c "import jwt, time; print(jwt.encode({'sub': 'alice', 'scopes': ['wallet:read', 'wallet:transfer'], 'iat': int(time.time()), 'exp': int(time.time())+86400}, 'local-dev-jwt-secret-key-32-bytes!', algorithm='HS256'))")

1. GET /me/balance

Retrieves the balance for the authenticated user context.

Headers:

  • Authorization: Bearer <TOKEN>

cURL:

curl -X GET http://127.0.0.1:3000/me/balance \
  -H "Authorization: Bearer $TOKEN"

Response (200 OK):

{
  "userId": "alice",
  "balance": 1400,
  "status": "ACTIVE"
}

2. POST /transfers

Executes an atomic transfer from caller to recipient.

Headers:

  • Authorization: Bearer <TOKEN>
  • Idempotency-Key: <unique-string>
  • Content-Type: application/json

Payload:

{
  "to": "bob",
  "amount": 100
}

cURL:

curl -X POST http://127.0.0.1:3000/transfers \
  -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: demo-key-101" \
  -H "Content-Type: application/json" \
  -d '{"to": "bob", "amount": 100}'

Response (201 Created):

{
  "status": "SUCCESS",
  "from": "alice",
  "to": "bob",
  "amount": 100,
  "idempotencyKey": "demo-key-101"
}

Replay Behavior (200 OK):

Submitting the exact same command again yields:

{
  "status": "DUPLICATE_ACCEPTED",
  "message": "Transfer previously completed",
  "idempotencyKey": "demo-key-101"
}

3. POST /admin/topup

Credits a user balance directly. Configured with native AWS IAM authorization (SigV4).

Headers:

  • Content-Type: application/json

(Bypasses JWT Bearer Token Authorizer)

Payload:

{
  "userId": "alice",
  "amount": 500
}

cURL:

curl -X POST http://127.0.0.1:3000/admin/topup \
  -H "Content-Type: application/json" \
  -d '{"userId": "alice", "amount": 500}'

Response (200 OK):

{
  "status": "TOPUP_SUCCESS",
  "userId": "alice",
  "newBalance": 1900
}

Error Handling Matrix

The application maps internal transaction failures to RFC-compliant HTTP status codes:

HTTP Status Error Scenario Trigger Condition
400 Bad Request Input Validation Error Missing headers, negative amount, self-transfers, or malformed JSON
401 Unauthorized Token Failure Expired JWT signature, invalid formatting, or missing bearer token
403 Forbidden Scope Denied / Account Frozen Missing wallet:transfer scope or target account is marked INACTIVE
404 Not Found Missing Entity Recipient or sender username does not exist in ledger table
409 Conflict Transaction Race / Key Conflict Concurrent modification conflict or foreign idempotency key collision
422 Unprocessable Insufficient Funds Sender wallet balance is lower than the requested transfer amount
500 Server Error Unhandled System Fault Downstream DynamoDB client disconnect or unexpected runtime fault

Project Structure

wallet-api-local-docker/
├── docker-compose.yml       # DynamoDB Local definition with bridge network
├── template.yaml            # CloudFormation / SAM Infrastructure & IAM policies
├── env.json                 # SAM environment variable overrides
├── Makefile                 # Automated commands for developer workflow
├── requirements.txt         # Runtime Lambda dependencies (pyjwt)
├── src/
│   ├── authorizer/
│   │   └── app.py           # Custom Token Authorizer & IAM Policy Generator
│   ├── handlers/
│   │   ├── balance.py       # GET /me/balance handler
│   │   ├── transfer.py      # POST /transfers handler
│   │   └── admin_topup.py   # POST /admin/topup handler (IAM auth)
│   └── shared/
│       ├── db.py            # TransactWriteItems builder & DynamoDB logic
│       ├── errors.py        # Domain exceptions & HTTP error mapper
│       └── utils.py         # Response serializer & test JWT generator
└── tests/
    ├── seed_data.py         # DynamoDB table initialization & seed accounts
    └── test_api.py          # Complete integration test suite

Cleanup

To stop containers and wipe local database state:

make clean

About

An enterprise-grade, event-driven payment backend engine emulating core ledger transactions. Built using Python 3.11, AWS Lambda, Amazon API Gateway, and DynamoDB Local, fully orchestrated via Docker and AWS SAM CLI for reproducible local evaluation without an AWS account.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages