system design

Low Level Design: Plan Management Service

What Is a Plan Management Service?

A Plan Management Service owns the catalog of subscription plans and the logic for upgrading, downgrading, and switching between them. It is a supporting service to the subscription lifecycle: while the subscription service tracks which plan a customer is on, the plan management service defines what plans exist, what they cost, what features they include, and how mid-cycle plan changes are prorated.

Data Model

-- Plan catalog
CREATE TABLE plans (
    plan_id        INT PRIMARY KEY AUTO_INCREMENT,
    code           VARCHAR(50) NOT NULL UNIQUE,   -- e.g. 'pro_monthly'
    display_name   VARCHAR(100) NOT NULL,
    price_cents    INT NOT NULL,
    currency       CHAR(3) NOT NULL DEFAULT 'USD',
    interval       ENUM('monthly','annual') NOT NULL,
    trial_days     INT NOT NULL DEFAULT 0,
    is_public      BOOLEAN NOT NULL DEFAULT TRUE,
    created_at     TIMESTAMP NOT NULL DEFAULT NOW()
);

-- Feature entitlements per plan
CREATE TABLE plan_features (
    plan_id        INT NOT NULL REFERENCES plans(plan_id),
    feature_key    VARCHAR(100) NOT NULL,
    feature_value  VARCHAR(255) NOT NULL,   -- e.g. 'seats=10', 'storage_gb=100'
    PRIMARY KEY (plan_id, feature_key)
);

-- Change history (proration ledger)
CREATE TABLE plan_changes (
    change_id         BIGINT PRIMARY KEY AUTO_INCREMENT,
    subscription_id   BIGINT NOT NULL,
    old_plan_id       INT NOT NULL,
    new_plan_id       INT NOT NULL,
    effective_at      TIMESTAMP NOT NULL,
    proration_cents   INT NOT NULL DEFAULT 0,   -- positive = credit, negative = charge
    reason            ENUM('upgrade','downgrade','admin') NOT NULL,
    created_at        TIMESTAMP NOT NULL DEFAULT NOW()
);

Core Algorithm: Proration

When a customer switches plans mid-cycle, the service must calculate a prorated credit or charge. The standard algorithm:

  • Compute days remaining in the current period: days_left = (period_end - NOW()) / 86400.
  • Compute total days in the period: period_days = (period_end - period_start) / 86400.
  • Credit for unused old plan: credit = old_price_cents * (days_left / period_days).
  • Charge for new plan remainder: charge = new_price_cents * (days_left / period_days).
  • Net proration: charge - credit. Positive = bill now. Negative = apply as account credit.

All arithmetic is done in integer cents with explicit rounding rules (round half-up) documented and tested. Never use floating-point for money.

Plan Change Workflow

  • An immediate upgrade applies the new plan now and charges the proration immediately.
  • A downgrade at period end sets pending_plan_id on the subscription and applies at renewal to avoid prorating a lower price mid-cycle.
  • An admin override supports zero-proration plan moves for support cases, recorded with reason='admin' and an actor user ID.

Failure Handling

Plan changes that include a charge must wrap the proration payment and the subscription update in a distributed saga: charge first, then update the subscription. If the payment fails, the plan change is rolled back and the customer remains on their current plan. Store plan_changes rows with a status column (pending, applied, failed) and process them transactionally.

Scalability Considerations

  • The plan catalog is small and read-heavy. Cache the full catalog in application memory, refreshed every 5 minutes. A single cache miss is acceptable; stampede protection is not needed at this scale.
  • Feature entitlement lookups (what can this user do?) should be served from the cache, not a per-request DB join.
  • Plan changes are infrequent relative to reads. A write to plan_changes plus a subscription update is the hot write path; a single writer per subscription (using optimistic locking on a version column) prevents concurrent change conflicts.

See also: Stripe Interview Guide 2026: Process, Bug Bash Round, and Payment Systems

See also: Shopify Interview Guide

See also: Scale AI Interview Guide 2026: Data Infrastructure, RLHF Pipelines, and ML Engineering

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