Back to Amazon questions
CodingSoftware Engineer

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

TierPoints RequiredBenefit
Standard0+Baseline — 1 point per $1 spent
Silver500+1.5x point multiplier, free delivery
Gold2000+2x point multiplier, free delivery + 5% cashback
Platinum5000+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.STANDARD

Implementation

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_RATE

Example 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

OperationTimeSpace
add_customerO(1)O(1)
record_purchaseO(T) = O(1) — T tiers is constantO(1)
redeem_pointsO(1)O(1)
get_walletO(1)O(1)
get_benefitsO(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 points and lifetime_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 PointsLedger with source/destination fields.