I built a FastAPI-based Reward Decision Service that returns deterministic reward outcomes for transactions. The service is idempotent, config-driven, and uses Redis for caching with in-memory fallback.
- POST /reward/decide - main endpoint for reward decisions
- GET /health - health check endpoint
- Request validation using Pydantic models
- Response includes decision_id, policy_version, reward_type, reward_value, xp, reason_codes, and meta
- Idempotency key: txn_id + user_id + merchant_id
- Same request returns exact same response
- TTL configurable (default 24 hours)
- Implemented in IdempotencyService
- Four strategies: PREFIX, JSON_FILE, IN_MEMORY, MOCK_ENDPOINT
- Default is PREFIX strategy (new_, ret_, pow_ prefixes)
- Three personas: NEW, RETURNING, POWER
- Each persona has different XP multipliers and daily CAC caps
- Implemented in PersonaIdentifier service
- Three reward types: XP, CHECKOUT, GOLD
- XP always given alongside monetary rewards
- Weighted random selection based on config
- Formula: xp = min(amount * xp_per_rupee * persona_multiplier, max_xp_per_txn)
- Monetary rewards: CHECKOUT (10%), GOLD (5%) of transaction amount
- Daily CAC cap per persona (NEW: 100, RETURNING: 300, POWER: 500)
- Cooldown period between monetary rewards (5 minutes)
- Prefer XP mode flag
- Atomic CAC reservation to prevent race conditions
- Downgrades to XP if rules violated
- Redis as primary cache
- In-memory fallback if Redis unavailable
- Cache keys: idem:, last_reward:, cac:*
- CacheManager handles both implementations
- Atomic check-and-increment for CAC tracking
- YAML-based policy configuration (config/policy.yaml)
- Environment variables via .env file
- Configurable reward weights, XP rates, caps, feature flags
- PolicyConfig class loads and validates config
app/
- main.py - FastAPI application entry point
- api/endpoints.py - API route handlers
- core/config.py - configuration management
- core/cache.py - cache manager with Redis and in-memory
- models/request.py - request models
- models/response.py - response models
- models/enums.py - RewardType, Persona, ReasonCode enums
- services/reward_decision.py - main business logic
- services/idempotency.py - idempotency handling
- services/persona_identifier.py - persona identification
- utils/helpers.py - utility functions
config/
- policy.yaml - reward policy configuration
tests/
- unit/ - 96 unit tests
- integration/ - 4 integration tests
- load/ - K6 load tests
- test_helpers.py - 18 tests for utility functions
- test_enums.py - 24 tests for enum classes
- test_persona_identifier.py - 20 tests for persona strategies
- test_idempotency_service.py - 11 tests for idempotency
- test_reward_decision_service.py - 23 tests for reward logic
- All dependencies mocked
- Tests run in 0.20 seconds
- test_api_server.py - API endpoint testing
- test_cac_race_condition.py - concurrent CAC updates
- test_cooldown.py - cooldown period validation
- test_determinism.py - idempotency verification
- Uses real service instances with mocked cache
- K6 smoke test - basic functionality check
- K6 load test - sustained load testing
- K6 stress test - breaking point testing
- Setup script for K6 installation
- Documentation for running tests
- Dockerfile for FastAPI service
- docker-compose.yml with Redis and web service
- Multi-worker deployment with Gunicorn
- Health checks for both services
- Persistent Redis data volume
- Network isolation
- Async/await throughout the codebase
- uvloop for faster event loop
- httptools for faster HTTP parsing
- orjson for faster JSON serialization
- Cache-first design
- Atomic operations to avoid locks
- Function names: camelCase
- Variable names: snake_case
- Type hints on all functions
- Docstrings for all public methods
- Pydantic for data validation
- Same txn_id always returns same response via idempotency cache
- Random reward selection happens once, then cached
- Decision ID generated once and stored
- Atomic check-and-increment operation
- Daily reset based on date
- Per-user, per-day tracking
- Prevents race conditions with multiple concurrent requests
- Tracks last monetary reward timestamp
- Checks time difference before allowing new reward
- Can be disabled via feature flag
- Only applies to monetary rewards, not XP
- Pluggable strategy pattern
- Default PREFIX strategy for simplicity
- Can switch strategies via environment variable
- Fallback to prefix if other strategies fail
.env - environment variables config/policy.yaml - reward policy data/personas.json - persona mappings (optional) requirements.txt - Python dependencies docker-compose.yml - Docker services
FastAPI - web framework Uvicorn - ASGI server Pydantic - data validation Redis - caching PyYAML - config parsing pytest - testing K6 - load testing
- API accepts requests and returns rewards
- Idempotency prevents duplicate processing
- Persona system identifies user types
- Business rules enforce caps and cooldowns
- Caching works with Redis or in-memory
- Tests validate all core functionality
- Docker deployment ready
- Load testing shows performance metrics
- No persistent database, only cache
- No authentication or authorization
- No rate limiting
- No logging or metrics
- No distributed locking for multi-instance deployment
- No retry logic for failures
- No circuit breakers
- Config changes require restart