Almost every consumer platform eventually ships tiers. Free and paid, basic and premium, standard and plus. It looks like one of the simpler features on the roadmap: add a field to the user record, gate a few things behind it, done.

Then it ships, and six months later the tier logic is scattered across a dozen services, nobody can say with confidence what a given user is entitled to, and changing the pricing requires a migration. This happens often enough that it is worth understanding why a feature that looks trivial turns out not to be.

Tiers are not a property of the user

The first mistake is modeling the tier as an attribute on the user or account record. It is the obvious move, and it is wrong for a reason that only becomes clear later.

A tier is a relationship between a user and a set of entitlements during a period of time. All three parts matter. Users change tiers. Entitlements change independently of users. And both changes need to be queryable historically, because support will eventually ask what someone was entitled to on a specific date in the past, and billing disputes depend on the answer.

If user.tier = ‘premium’ is your model, you have no history, no scheduled changes, and no way to represent a user who is mid-transition. Those limitations each generate a workaround, and the workarounds are what turn the feature into a mess.

The better shape is a membership or subscription record with a start date, an optional end date, and a reference to a tier definition that is itself versioned. Verbose, and it answers every question the simpler model cannot.

Entitlements need to be data, not code

The second failure is expressing entitlements as conditional logic scattered through the application. Checks like if user.tier == ‘premium’ sprinkled across services work fine until the business adds a fourth tier, grandfathers a group of legacy accounts, or runs a promotion granting one premium feature to basic users.

At that point every one of those conditionals needs auditing, and you will miss some.

Entitlements belong in a table, evaluated at the point of use. The application asks whether this user can do this thing, and something authoritative answers. The business can then add tiers, adjust what each includes, and handle exceptions without a deploy.

This is more infrastructure than most teams want to build for version one. It is also the thing teams most consistently wish they had built when the third tier arrives.

Grandfathering is the requirement nobody plans for

Every pricing change creates a population of users on terms that no longer exist. You cannot avoid this and you cannot usually migrate them, because changing someone’s terms unilaterally is both a trust problem and sometimes a legal one.

So the system must be able to represent arrangements that are no longer offered. That means tier definitions are immutable once issued, and pricing changes create new versions rather than editing existing ones. Users stay attached to the version they signed up under until something explicitly moves them.

Teams that treat tier definitions as editable configuration discover this the hard way, usually by accidentally repricing a cohort of long-standing customers.

Flexible ordering breaks fixed-tier thinking

Tiers get harder when the underlying relationship is not a flat recurring fee for a fixed bundle.

Membership structures in physical goods are a useful case. The membership typically determines pricing and standing across a catalog rather than granting access to a defined set of features, and the member decides what to order and when. Melaleuca membership options work along these lines, where the membership establishes the relationship and the ordering happens underneath it on the household’s own cadence.

That shape does not map cleanly onto a tier field. Entitlement is per-product and dynamic, order volume varies independently of membership status, and a member can be fully active while ordering nothing in a given month. Any model that infers standing from recent activity will get this wrong.

The general lesson applies beyond physical goods: if your entitlements vary by what the user is doing rather than only by what plan they are on, a flat tier enum is already insufficient and you should skip straight to the entitlements table.

Catalog scale changes the design

Tier systems that work at ten features break at a thousand SKUs.

Melaleuca carries products across cleaning, laundry, personal care, and supplements, which means member pricing applies across hundreds of items rather than unlocking a handful of capabilities. At that scale you are not checking a boolean, you are running a pricing calculation per line item against a rule set that varies by membership status and possibly by tenure or volume.

Teams building for that scale usually land on a pricing service separate from both the catalog and the membership record. The catalog knows what things are, the membership knows the customer’s standing, and the pricing service combines them at request time. Denormalizing member prices onto the product records is tempting for performance and painful the first time pricing rules change.

State transitions are where the bugs live

The interesting failures in tier systems are almost never in the steady states. They are in the transitions.

A user upgrading mid-period. A downgrade scheduled for the end of the current term. A payment failure that should suspend but not terminate. A reactivation after a lapse, where the question of whether they return to their old tier or the current equivalent has real consequences. A pause with a defined return date.

Each of these is a state with its own entry and exit conditions, and each is a place where entitlement and billing can disagree. The teams that handle this well write the state machine explicitly and test the transitions rather than the states. The teams that do not end up with users who are being billed for something they cannot access, which is the single most expensive bug class in this area.

What to build first

For a team shipping tiers for the first time, the minimum that will not need rebuilding:

A membership record separate from the user record, with start and end dates. Versioned tier definitions that are immutable once issued. An entitlements table queried at the point of use rather than conditionals in application code. An explicit state machine covering upgrade, downgrade, suspend, reactivate, and pause. And historical queryability, so you can answer what any user was entitled to on any past date.

That is more than a field on the user table, and considerably less than what you will build in a panic eighteen months from now when the business asks for something the simple version cannot express.

The underlying point

Tiers look simple because the common case is simple. One user, one plan, no changes. The complexity lives entirely in the exceptions, and the exceptions are guaranteed rather than hypothetical. Pricing will change. Legacy cohorts will exist. Someone will need to be in a state your enum does not cover.

Building for that from the start costs maybe an extra week. Retrofitting it costs a quarter and a migration nobody wants to run.