Back to blog

Designing a plan system that you can actually change later

Plan definitions evolve: new tiers, grandfathering, per-customer overrides. Here's how to store and version plans so you can change them without breaking production.

XLinkedIn

Your first plan structure is never your last. You'll inevitably:

  • Add tiers
  • Change prices
  • Run experiments
  • Support enterprise custom terms

The problem with rigid plan systems

If your plan system is hard-coded or tightly coupled to today's schema, every change becomes:

  • A risky migration
  • A fear of breaking existing customers

Design for evolution from the start

This saves you from rigid schemas and "we can't change that" dead ends. That means:

  • Versioned or time-bound plan definitions
  • A clear separation between "what the plan is" and "what this customer has"

This article covers how to model plans so you can change them later: storing definitions, handling grandfathering, supporting per-customer overrides, and avoiding the traps that lock you in.

Why plan systems get rigid

The naive approach

Bake plans into the schema:

  • A plan_id or plan_name on the account
  • Application code that says "if plan is X, limit is Y"

The problems

When you want to add a plan, change a limit, or run a price experiment:

  • You're changing code or doing a data migration

When you want to grandfather existing customers on an old plan:

  • You need special cases: "if plan is legacy_pro, use old limits"

The logic spreads across the codebase, and every change risks regressions.

The root cause

Treating plan definition (what a plan is) and plan assignment (what this customer has) as the same thing, and storing too little context:

  • "Which version of Pro?"
  • "When did this plan start?"

The solution

To change plans over time, you need to:

  • Separate definition from assignment
  • Keep enough history to know what rules applied when

Store plan definitions, not just plan names

A plan definition describes what a plan is:

  • Name and display name
  • Allowances (e.g., 10,000 credits/month)
  • Limits
  • Features
  • Pricing

Plans evolve over time

You might have "Pro v1" (10k credits) and "Pro v2" (15k credits, new price). If you only store plan_id = pro on the account, you can't tell which version they're on.

Two approaches to versioning

1. Versioned definitions:

  • Each plan has a version or effective date
  • The account stores "plan_id + version" or "plan_id + effective_from"
  • When you add Pro v2, existing customers keep Pro v1 until they're migrated; new signups get Pro v2
  • You can query "what limits does this account have?" by resolving plan + version

2. Time-bound or immutable definitions:

  • When you change a plan, you don't mutate the old definition
  • You create a new one (new id or new version) and assign new signups to it
  • Existing assignments keep pointing at the old definition
  • "Grandfathering" is natural: they keep the old definition until you explicitly migrate them

The key principle

The thing stored on the account references a definition that can be versioned or replaced, so changing "what Pro means" for new customers doesn't force you to change what existing Pro customers have.

Grandfathering and migrations

What grandfathering means

Existing customers keep their current plan terms; new customers get the new terms.

How versioned definitions enable this

To support that, you must be able to say:

  • "This account is on this definition" (the old one)
  • "New signups get that definition" (the new one)

That's exactly what versioned or immutable definitions give you.

Migration as an explicit operation

Migration is then an explicit operation:

  • Run a job or support flow
  • Move selected accounts from definition A to definition B
  • Do this on a schedule or at renewal

You're not overloading a single "Pro" row with "sometimes old limits, sometimes new." You're moving accounts from one definition to another when it's correct to do so.

Per-customer overrides

The need for custom terms

Enterprise and large deals often need:

  • A custom credit allowance
  • A custom price
  • A custom cap

The naive approach (don't do this)

If your system only knows "plan_id," you end up with special-case columns:

  • custom_limit
  • custom_price

And logic that says "if custom_limit is set, use that, else use plan default." That works for one override type but gets messy with many.

A cleaner approach: layers

Treat overrides as layers:

  • The account has a base plan (with a specific definition/version)
  • Optionally an override (e.g., "allowance override: 50,000")

Resolution logic

For this account:

  1. Start with the plan definition
  2. Apply overrides

Storage

Overrides can be stored as:

  • Key-value pairs
  • A small override table (account_id, override_type, value, optional effective range)

Benefits

  • Add new override types without changing the core plan model
  • Report "base plan vs. overridden" for analytics and finance

Experiments and A/B tests

The challenge

To A/B test a new plan or price, you need to assign some accounts to variant A and some to B without rewriting production logic for every variant.

How versioned definitions help

Variant A and B are two definitions (or two versions of the same logical plan).

  • Accounts are assigned to one or the other
  • Your code just resolves "what plan does this account have?" and applies that definition

Keep experiments separate from the engine

The experiment is a matter of:

  • Assignment
  • Analytics

Not of branching inside the credits engine. That keeps the credits system stable while product and GTM run experiments.

Summary and recommendations

Design your plan system so that:

Core principles

  • Definitions are versioned or immutable so you can add "Pro v2" without breaking "Pro v1" for existing customers
  • Accounts reference a specific definition (and optionally effective date) so you know exactly what rules apply to them
  • Overrides are a separate layer so custom terms don't pollute the core plan schema
  • Migration is an explicit operation so grandfathering and experiments are controlled and auditable

Build vs. buy

If you're building this yourself, budget for the extra complexity of:

  • Versioning
  • Override resolution

If you're evaluating a credits and plans API, look for first-class support for:

  • Plan versions
  • Effective dates
  • Overrides

Then you can change plans and run experiments without turning your codebase into a maze of conditionals.

credits.dev is built with plans and scheduling in mind, so you can evolve your offerings over time without locking yourself into a rigid schema.