Low-latency FastAPI microservice that returns deterministic reward decisions for transactions.
- POST
/reward/decideAPI with request/response validation - Deterministic reward decisioning using config-driven rules
- Persona-based reward logic with multipliers (NEW, RETURNING, POWER)
- Deterministic reward selection using hash-based weighted distribution
- Idempotency (
txn_id + user_id + merchant_id) with lock/wait handling - Distributed locking to ensure safe concurrent processing
- Cache-first architecture with Redis backend and in-memory fallback
- Daily CAC cap enforcement with XP fallback
- Atomic CAC budget tracking to prevent race conditions
- Reward cooldown mechanism to limit frequent monetary rewards
- Feature flags:
prefer_xp_modecooldown_on_last_reward
- Router-level rate limiting with
Retry-Afterheader - Unit tests using pytest
- Custom threaded load-test script with latency (p50, p95, p99) and throughput metrics
Flow for POST /reward/decide:
app/
app.py
routers/reward.py
services/reward_service.py
models/schemas.py
cache/cache.py
core/config.py
config/
policy.json
tests/
test_reward_service.py
scripts/
load_test.py
docs/
images/
.gitkeep
performance_report_template.md
Request fields:
txn_id(string)user_id(string)merchant_id(string)amount(number, > 0)txn_type(string)ts(string)
Response fields:
decision_id(UUID string)policy_version(string)reward_type(XP/CHECKOUT/GOLD)reward_value(integer)xp(integer)reason_codes(list of strings)meta(object)
Policy is loaded from config/policy.json by default.
Override path with:
export POLICY_CONFIG_PATH=/absolute/path/to/policy.jsonCache/rate-limit environment variables:
export CACHE_BACKEND=memory # memory | redis
export REDIS_HOST=localhost
export REDIS_PORT=6379
export REDIS_DB=0
export RATE_LIMIT_MAX_REQUESTS=30
export RATE_LIMIT_WINDOW_SECONDS=60docker compose up --buildServices:
- API:
http://localhost:8000 - Redis:
localhost:6379
Stop containers:
docker compose downOptional: create a .env file if you want custom values (not required).
If no .env is provided, docker-compose.yml defaults are used.
You can start from .env.example.
Build:
docker build -t reward-decision-service .Run (expects Redis already available):
docker run --rm -p 8000:8000 \
-e CACHE_BACKEND=redis \
-e REDIS_HOST=host.docker.internal \
-e REDIS_PORT=6379 \
reward-decision-servicepython3 -m venv .venv
source .venv/bin/activate
pip install -e .
pip install pytestuvicorn main:app --host 0.0.0.0 --port 8000 --reloadSwagger docs:
http://localhost:8000/docs
curl -X POST "http://localhost:8000/reward/decide" \
-H "Content-Type: application/json" \
-d '{
"txn_id": "txn-1",
"user_id": "user_1",
"merchant_id": "m1",
"amount": 100,
"txn_type": "PURCHASE",
"ts": "2026-03-18T12:00:00Z"
}'python3 -m pytest -qpython3 scripts/load_test.py \
--url http://localhost:8000/reward/decide \
--duration 30 \
--workers 120 \
--timeout 2 \
--user-pool 10000Outputs include:
attempt_throughput_rpsresponse_throughput_rpslatency_p50_mslatency_p95_mslatency_p99_msstatus_countserror_counts
Use docs/performance_report_template.md for reporting.
- Persona is mocked in service with an in-memory map and cached per user.
- Time-based counters (CAC/rate limit) are handled in UTC.
- Idempotent response consistency is guaranteed within configured idempotency TTL.
- Redis is optional for local development due to in-memory fallback.