Loyalty Program Points System
Role: Software Engineer
Problem Overview
Design a loyalty program system for Amazon Fresh shoppers. The system must:
- Award points when customers shop
- Track total points in a points wallet per customer
- Manage tier-based benefits based on accumulated points
- Allow customers to redeem points for discounts/rewards
Tiers
| Tier | Points Required | Benefit |
|---|---|---|
| Standard | 0+ | Baseline — 1 point per $1 spent |
| Silver | 500+ | 1.5x point multiplier, free delivery |
| Gold | 2000+ | 2x point multiplier, free delivery + 5% cashback |
| Platinum | 5000+ | 3x point multiplier, free delivery + 10% cashback + priority support |
API
python
# Add a customer to the system
add_customer(customer_id: str) -> None
# Record a purchase and award points
record_purchase(customer_id: str, amount_dollars: float) -> int # returns points earned
# Redeem points for a discount
redeem_points(customer_id: str, points: int) -> bool # returns success
# Get current points and tier
get_wallet(customer_id: str) -> Wallet
# Get available benefits for a customer
get_benefits(customer_id: str) -> list[str]Design
Data Model
python
from enum import Enum
from dataclasses import dataclass, field
class Tier(Enum):
STANDARD = "Standard"
SILVER = "Silver"
GOLD = "Gold"
PLATINUM = "Platinum"
TIER_THRESHOLDS = [
(5000, Tier.PLATINUM),
(2000, Tier.GOLD),
(500, Tier.SILVER),
(0, Tier.STANDARD),
]
TIER_MULTIPLIERS = {
Tier.STANDARD: 1.0,
Tier.SILVER: 1.5,
Tier.GOLD: 2.0,
Tier.PLATINUM: 3.0,
}
TIER_BENEFITS = {
Tier.STANDARD: ["1 point per $1 spent"],
Tier.SILVER: ["1 point per $1 spent", "1.5x point multiplier", "Free delivery"],
Tier.GOLD: ["1 point per $1 spent", "2x point multiplier", "Free delivery", "5% cashback"],
Tier.PLATINUM: ["1 point per $1 spent", "3x point multiplier", "Free delivery", "10% cashback", "Priority support"],
}
# Points-to-dollar redemption rate: 100 points = $1
POINTS_PER_DOLLAR = 1
REDEMPTION_RATE = 100 # points per $1 discount
@dataclass
class Wallet:
customer_id: str
points: int = 0
tier: Tier = Tier.STANDARD
lifetime_points: int = 0 # used for tier calculation (never decremented)
def get_tier(self) -> Tier:
for threshold, tier in TIER_THRESHOLDS:
if self.lifetime_points >= threshold:
return tier
return Tier.STANDARDImplementation
python
class LoyaltyProgram:
def __init__(self):
self._wallets: dict[str, Wallet] = {}
def add_customer(self, customer_id: str) -> None:
if customer_id in self._wallets:
raise ValueError(f"Customer {customer_id} already exists")
self._wallets[customer_id] = Wallet(customer_id=customer_id)
def _get_wallet(self, customer_id: str) -> Wallet:
if customer_id not in self._wallets:
raise KeyError(f"Customer {customer_id} not found")
return self._wallets[customer_id]
def record_purchase(self, customer_id: str, amount_dollars: float) -> int:
"""
Award points for a purchase. Points = floor(amount × multiplier).
Tier is based on lifetime points (not affected by redemptions).
Returns the number of points earned.
"""
wallet = self._get_wallet(customer_id)
multiplier = TIER_MULTIPLIERS[wallet.tier]
points_earned = int(amount_dollars * POINTS_PER_DOLLAR * multiplier)
wallet.points += points_earned
wallet.lifetime_points += points_earned
# Update tier based on lifetime points
wallet.tier = wallet.get_tier()
return points_earned
def redeem_points(self, customer_id: str, points: int) -> bool:
"""
Redeem points for a discount. 100 points = $1.
Returns True if successful, False if insufficient points.
Redemption does NOT reduce lifetime_points (tier is preserved).
"""
wallet = self._get_wallet(customer_id)
if wallet.points < points:
return False
wallet.points -= points
return True
def get_wallet(self, customer_id: str) -> Wallet:
return self._get_wallet(customer_id)
def get_benefits(self, customer_id: str) -> list[str]:
wallet = self._get_wallet(customer_id)
return TIER_BENEFITS[wallet.tier]
def get_discount_value(self, points: int) -> float:
"""Convert points to dollar discount."""
return points / REDEMPTION_RATEExample Walkthrough
python
program = LoyaltyProgram()
program.add_customer("cust_001")
# Customer makes purchases
points = program.record_purchase("cust_001", 100.0) # 100 pts (Standard tier)
print(points) # 100
points = program.record_purchase("cust_001", 800.0) # 800 pts (now Silver: 1.5x)
# Wait: 100 lifetime pts → still Standard. 100 * 1.0 = 100 pts for $100 purchase.
# After: lifetime=100, tier=Standard. $800 * 1.0 = 800 pts.
# Now lifetime=900 → Silver tier kicks in.
print(points) # 800
wallet = program.get_wallet("cust_001")
print(wallet.tier) # Tier.SILVER
print(wallet.points) # 900
# Next purchase uses Silver multiplier
points = program.record_purchase("cust_001", 200.0) # 200 * 1.5 = 300 pts
print(points) # 300
# lifetime = 1200 → still Silver
# Redeem some points
success = program.redeem_points("cust_001", 500)
print(success) # True
print(wallet.points) # 700 (redeemed 500, lifetime stays at 1200)
print(wallet.tier) # Tier.SILVER (lifetime unchanged)
print(program.get_discount_value(500)) # 5.0 ($5 discount)
# Benefits
print(program.get_benefits("cust_001"))
# ["1 point per $1 spent", "1.5x point multiplier", "Free delivery"]Design Discussion Points
Why track lifetime_points separately from points?
Redemptions shouldn't demote a customer's tier — otherwise customers would be penalized for using their rewards. Tier is based on total points ever earned (lifetime_points), while points is the current spendable balance.
How would you handle tier downgrades?
Many loyalty programs have an annual reset: if a customer doesn't reach a threshold within a calendar year, they're downgraded. Add a tier_year field and reset annual_points each January.
Point expiration?
Store points as timestamped entries: [(timestamp, points), ...]. On each operation, expire entries older than N days.
Complexity Analysis
| Operation | Time | Space |
|---|---|---|
add_customer | O(1) | O(1) |
record_purchase | O(T) = O(1) — T tiers is constant | O(1) |
redeem_points | O(1) | O(1) |
get_wallet | O(1) | O(1) |
get_benefits | O(1) | O(1) |
Follow-Up Questions
- How would you handle concurrent purchases from the same customer? Use optimistic locking or a per-customer lock to prevent race conditions on
pointsandlifetime_points. - How would you scale to millions of customers? Shard by
customer_id, cache wallet state in Redis, persist to a database. - How would you add a referral bonus? Add
record_referral(referrer_id, new_customer_id)which credits the referrer with a fixed bonus. - How would you support partner point transfers (earn at Whole Foods, spend at Amazon Fresh)? Abstract points into a
PointsLedgerwith source/destination fields.