system design

Price Alert System Low-Level Design

What is a Price Alert System?

A price alert system notifies users when a tracked item’s price drops below their target threshold. Used by Amazon, Google Shopping, Honey, and Camelcamelcamel. The core challenge: millions of users have set alerts on millions of products. When any product’s price changes, efficiently find and notify only the users whose threshold has been crossed — without scanning all alerts.

Requirements

  • Users set price alerts: “notify me when product X drops below $Y”
  • Process 100K product price changes per hour
  • Evaluate millions of alerts per price change in <5 seconds
  • Send notifications via email, push, and in-app
  • Deduplicate: don’t notify the same user twice for the same alert until the price recovers above their threshold
  • Alert management: list, pause, delete alerts

Data Model

PriceAlert(
    alert_id    UUID PRIMARY KEY,
    user_id     UUID NOT NULL,
    product_id  UUID NOT NULL,
    target_price DECIMAL(10,2) NOT NULL,   -- notify when price <= this
    status      ENUM(ACTIVE, TRIGGERED, PAUSED, DELETED),
    created_at  TIMESTAMPTZ,
    last_triggered_at TIMESTAMPTZ,
    INDEX (product_id, target_price) -- critical for efficient evaluation
)

ProductPrice(
    product_id  UUID PRIMARY KEY,
    current_price DECIMAL(10,2),
    previous_price DECIMAL(10,2),
    updated_at  TIMESTAMPTZ
)

AlertNotification(
    notification_id UUID PRIMARY KEY,
    alert_id    UUID,
    user_id     UUID,
    product_id  UUID,
    triggered_price DECIMAL(10,2),
    sent_via    VARCHAR[],  -- ['email', 'push']
    sent_at     TIMESTAMPTZ
)

Efficient Alert Evaluation

When product X’s price changes to $45: find all ACTIVE alerts for product X with target_price >= $45 (i.e., threshold crossed).

def evaluate_alerts(product_id, new_price, old_price):
    if new_price >= old_price:
        return  # price went up — no alerts to trigger

    # Find all alerts whose threshold is now crossed
    alerts = db.query('''
        SELECT * FROM PriceAlert
        WHERE product_id = :pid
          AND status = 'ACTIVE'
          AND target_price >= :new_price
        ORDER BY target_price DESC
    ''', pid=product_id, new_price=new_price)

    for alert in alerts:
        trigger_alert(alert, new_price)

def trigger_alert(alert, triggered_price):
    # Mark as triggered (deduplication — won't fire again until price recovers)
    db.update(PriceAlert, alert.id, status='TRIGGERED',
              last_triggered_at=now())

    # Queue notification
    notification_queue.enqueue({
        'user_id': alert.user_id,
        'alert_id': alert.alert_id,
        'product_id': alert.product_id,
        'triggered_price': triggered_price,
        'target_price': alert.target_price
    })

Price Recovery Reset

Once triggered, the alert should re-arm when the price recovers above the threshold:

def on_price_change(product_id, new_price, old_price):
    evaluate_alerts(product_id, new_price, old_price)

    # Re-arm triggered alerts if price recovered above threshold
    if new_price > old_price:
        db.execute('''
            UPDATE PriceAlert SET status='ACTIVE'
            WHERE product_id=:pid
              AND status='TRIGGERED'
              AND target_price < :new_price
        ''', pid=product_id, new_price=new_price)

Handling Large Products (Hot Items)

For a popular product with 500K active alerts (e.g., iPhone), evaluating all on every price change is expensive. Optimize:

# Redis sorted set: product → alerts indexed by target_price
# Key: alerts:{product_id}
# Score: target_price
# Member: alert_id

# On price drop to $45: find all alerts with target_price >= 45
triggered_alerts = redis.zrangebyscore(f'alerts:{product_id}', 45, '+inf')

# Process in batches of 1000; publish to Kafka for async notification
for batch in chunks(triggered_alerts, 1000):
    kafka.produce('alert-triggers', {'product_id': product_id,
                                     'price': 45.0,
                                     'alert_ids': batch})

Key Design Decisions

  • Index on (product_id, target_price) — enables fast range query for alerts whose threshold was crossed
  • TRIGGERED status for deduplication — prevents repeat notifications until price recovers above threshold
  • Redis sorted set for hot products — O(log n + k) query for alerts with target_price >= new_price; faster than DB for products with many alerts
  • Async notification via queue — decouples alert evaluation (fast, synchronous) from notification delivery (slower, may fail)
  • Price recovery re-arms alerts — users don’t need to manually reset; alert re-triggers on the next price drop

Price alert and product tracking system design is discussed in Amazon system design interview questions.

Price alert and e-commerce notification systems are discussed in Shopify system design interview preparation.

Price tracking and alert notification system design is covered in Google system design interview guide.

newsletter

What's actually being asked right now

Interview patterns & comp trends, straight to your inbox.

No spam. Unsubscribe anytime.

newsletter

What's actually being asked right now

Interview patterns & comp trends, straight to your inbox.

No spam. Unsubscribe anytime.

1972 Soviet postage stamp commemorating the Mars 2 probe

worth a read

Mars For The Rest of Us — a weekly-or-more deep dive on the technical side of Mars exploration: rocket propulsion, microbiology, mission architecture, and everything in between. Written by Maciej Ceglowski.

Read it on Substack →
Scroll to Top