Low-Level Design: Subscription Box Service — Curation, Billing Cycles, Inventory Allocation, and Churn

Core Entities

Subscriber: subscriber_id, user_id, plan_id, status (ACTIVE, PAUSED, CANCELLED), billing_cycle_anchor, next_billing_date, shipping_address, preferences[], skip_count. Plan: plan_id, name, price_cents, billing_interval (MONTHLY, QUARTERLY), box_size (SMALL, MEDIUM, LARGE), items_per_box. Box: box_id, subscriber_id, billing_period (2024-01), status (PENDING, CURATED, PACKED, SHIPPED, DELIVERED), curation_notes, items[]. Product: product_id, name, category, cost_cents, inventory_quantity, tags[]. BoxItem: box_id, product_id, quantity, unit_cost_cents.

Billing Cycles

Subscription billing must be reliable and idempotent. Billing anchor: the day of month the customer first subscribed (anchor=15 -> billed on the 15th of each month). On billing date: (1) Verify subscriber is ACTIVE. (2) Check skip requests (subscriber requested to skip this month). (3) Attempt payment via payment provider using stored payment method. (4) On success: create a Box record for this billing period, mark billing period as paid. (5) On payment failure: retry with exponential backoff (retry on day+1, day+3, day+7). After 3 failed attempts: dunning email sequence (update payment method request), suspend the subscription. Idempotency: store (subscriber_id, billing_period) with a unique constraint — the billing job can safely retry without double-charging.

Box Curation

Curation selects which products go into each subscriber box based on preferences and inventory. Rule-based curation: match subscriber tags (e.g., “vegan”, “fitness”) to product tags. Exclude products previously sent to this subscriber (track history per subscriber). Ensure items_per_box products are selected, covering required categories (e.g., at least 1 snack, 1 beauty item). ML-based curation: collaborative filtering on subscriber-product rating history. Predict ratings for unseen products; select the top-N highest predicted rating products that pass inventory checks. Curation pipeline: run as a batch job 5-7 days before shipping date (to allow inventory locking). Generate a curation proposal; human review for QA; lock inventory on approval.

Inventory Allocation

Inventory must be reserved before boxes are packed. Challenge: with 100K subscribers and 20 product SKUs per box period, some products may sell out before all boxes are curated. Allocation strategy: (1) Run allocation in subscriber priority order (longest-tenured subscribers first). (2) For each box: decrement inventory.available for each selected product. If any product is out of stock: swap to the next best alternative (same category, same tags). (3) Reserve inventory atomically: UPDATE products SET reserved = reserved + qty, available = available – qty WHERE available >= qty. If 0 rows affected: out of stock, apply substitution logic. Track allocation conflicts (substitution rate) as a supply chain KPI.

Churn Prediction and Prevention

Churn prediction model features: number of boxes skipped in the last 3 months, days since last login, product rating average (low ratings predict churn), subscriber tenure, customer service contact frequency, payment failure history. Model: gradient boosting classifier trained on churned vs retained subscribers. Score all active subscribers monthly; flag high-risk subscribers (churn probability > 0.7). Retention actions: personalized discount offer (10% off next 3 months), survey to understand dissatisfaction, curation adjustment (swap categories the subscriber has rated poorly), pause offer (pause up to 2 months without cancellation). Track retention action effectiveness: A/B test offers against a control group. Store churn_score and risk_tier per subscriber in the subscriber table.

Skip and Pause Management

Subscribers can skip a month or pause for up to 3 months. Skip: before the billing cutoff (typically 5 days before billing date), subscriber requests a skip. Store skip_request for the current billing period. Billing job checks for skip requests before attempting payment. If skipped: no charge, no box for that period. Next billing date advances by one interval. Pause: set subscriber.status = PAUSED, pause_until = future_date. Billing job skips PAUSED subscribers. Resume: either automatically on pause_until date or manually. Resume sends a welcome-back email and schedules the next billing cycle. Limit skips: allow 2-3 skips per year (excess skips indicate a churn risk, trigger a retention workflow instead of allowing another skip).

Asked at: Shopify Interview Guide

Asked at: Stripe Interview Guide

Asked at: Airbnb Interview Guide

Asked at: DoorDash Interview Guide

Scroll to Top