VitalCard Rewards Engine¶
A rewards calculation engine for credit card transactions — built serverless-first on AWS Lambda, SQS, and DynamoDB, and designed around the assumption that things will fail mid-batch.
Browse the architecture View on GitHub
The problem¶
Rewards look like a simple multiplication problem — amount × rate = points — right up until you put them behind a real payment network.
Settlement files land in S3 in bursts of hundreds of thousands. Webhooks arrive out of order. Lambdas time out mid-batch and get retried. And unlike a cache miss or a dropped analytics event, a mistake here is financial: credit the same transaction twice and you have handed out points that a customer will spend and an accountant will eventually come asking about.
The rule engine in this project is deliberately boring — declarative JSON rules, four calculation types, dot-notation field paths. The interesting engineering is everywhere else: making sure each transaction is counted exactly once, at volume, while failures are happening.
What's actually interesting here¶
-
Exactly-once processing¶
Not "at-least-once and hope." Every event gets a deterministic idempotency key derived from its own contents, checked before any work is done. Batch progress is checkpointed every 100 events, so a Lambda that dies 800 events into a 1,000-event batch resumes rather than restarts — and resuming can't double-credit the first 800.
-
Explicit SQS acknowledgment¶
The obvious Lambda + SQS integration acknowledges messages implicitly when the handler returns — quietly deleting messages the handler never finished. This engine tracks receipt handles per message and acknowledges only the ones that provably succeeded, so partial batch failures retry the failures instead of losing them.
-
Batch-first data access¶
Naive per-transaction processing costs one Lambda invocation and one DynamoDB round trip each. Batching metadata and rule loads collapses 1,000 transactions into a single invocation and roughly 10 database operations — about a 100× reduction in DynamoDB calls.
-
Failure modes, enumerated¶
Lambda timeouts, memory exhaustion, cold starts, DynamoDB throttling, partial batch writes, network partitions — each documented with its blast radius, how you detect it in CloudWatch, and what the system does about it.
How it fits together¶
flowchart TB
subgraph sources [Event sources]
S3[S3 settlement files]
API[Real-time webhooks]
ML[ML enrichment pipeline]
end
subgraph ingest [Ingestion]
LAM1[Lambda ingesters]
end
subgraph queue [Durable buffer]
SQS[SQS + DLQ]
end
subgraph core [Core processing]
RBP[Resilient batch processor<br/>idempotency + checkpointing]
RULE[Rule engine]
end
subgraph store [DynamoDB]
EV[(Events)]
MD[(Metadata)]
RL[(Rewards ledger)]
end
S3 --> LAM1
API --> LAM1
ML --> LAM1
LAM1 --> SQS
SQS --> RBP
RBP --> RULE
RULE --> RL
RBP --> EV
MD --> RULE
Every stage between the queue and the ledger is retry-safe. The idempotency check sits in front of the rule engine, so a redelivered SQS message costs a single DynamoDB read and then stops.
The rule engine¶
Rules are declarative JSON, versioned in DynamoDB, and evaluated against a merged view of the event, its enriched metadata, and computed fields.
Conditions support comparison, set, string, regex, date, and logical operators, with dot-notation field paths that reach across the event and its metadata:
Getting started¶
git clone https://github.com/louisalexander/rewards-engine.git
cd rewards-engine
pip install -r requirements.txt
cp .env.example .env
pytest tests/unit/ -v
For local development, run DynamoDB Local and seed sample data:
Status¶
Alpha — a reference implementation, not a deployed production system
The core rule engine, batch processors, and resilience machinery are implemented and unit-tested. Test coverage is still partial and the infrastructure-as-code layer is in progress.
Read this as a worked example of how to build correctness-critical event processing on serverless primitives — not as something to point at your card network on Monday.
Licensed under the MIT License.