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.
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_idorplan_nameon 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_limitcustom_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:
- Start with the plan definition
- 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.