Most design systems die not from a single catastrophic failure, but from a thousand small cuts. Each one seems harmless — a one-off component here, an inconsistent token there. Over time, the system becomes so riddled with exceptions that it's easier to start from scratch than to fix what exists. We've built design systems that survived three years of growth across multiple products. Here's what actually matters.
Why Design Systems Fail
Before we talk about what works, let's talk about what doesn't. The failure modes are remarkably consistent across organizations:
The Documentation Graveyard
The team creates beautiful documentation. Nobody reads it. Within six months, the docs are outdated and misleading. New developers ignore them entirely. The system lives in Figma files and tribal knowledge, not in the documented patterns.
The Component Bloat
Every product team adds "just one more component" to the shared library. The library grows from 40 components to 200. Half of them are variants of the same thing. Nobody knows which ones are actually used. The build size doubles. Import times triple.
The Consistency Illusion
The design system looks consistent in the documentation. In production, every product has slightly different spacing, slightly different colors, slightly different interaction patterns. The "system" is a suggestion, not a constraint.
The Governance Vacuum
Nobody owns the design system. It's everyone's responsibility, which means it's no one's responsibility. Changes happen ad-hoc. Breaking changes ship without notice. The system fragments across teams.
Token Architecture
Design tokens are the foundation. Get them wrong, and everything built on top is unstable. Get them right, and consistency becomes automatic.
Three-Tier Token System
We use a three-tier token architecture that separates intent from implementation:
/* TIER 1: Global tokens (raw values) */
--color-blue-500: #3b82f6;
--color-gray-100: #f3f4f6;
--space-4: 1rem;
--font-size-base: 1rem;
/* TIER 2: Semantic tokens (intent) */
--color-primary: var(--color-blue-500);
--color-surface: var(--color-gray-100);
--space-component-gap: var(--space-4);
--text-body: var(--font-size-base);
/* TIER 3: Component tokens (scoped) */
--button-bg: var(--color-primary);
--button-padding: var(--space-component-gap);
--card-bg: var(--color-surface);
This structure means you can rebrand the entire product by changing Tier 1 tokens. You can adjust spacing for a specific product by overriding Tier 2. You never need to touch component code for visual changes.
Token Naming Conventions
Naming tokens is the hardest part of the system. Bad names create confusion. Good names make the system self-documenting. Our rules:
- Name by intent, not value —
--color-primary, not--color-blue - Use hierarchical naming —
--space-component-padding, not--padding - Avoid abbreviations —
--color-background, not--bg - Include units in the name when ambiguous —
--space-4means 4 units of the spacing scale
Component API Design
A component's API is its contract. A well-designed API is predictable, flexible, and hard to misuse. A bad API creates workarounds that bypass the system entirely.
Composition Over Configuration
Instead of creating a Button component with 15 props for every possible variation, compose behavior from smaller primitives:
// ❌ Bad: Configuration explosion
<Button
variant="primary"
size="lg"
icon="arrow-right"
iconPosition="right"
loading={false}
disabled={false}
fullWidth={false}
rounded={true}
gradient={true}
/>
// ✅ Good: Composition
<Button variant="primary" size="lg">
<Button.Icon position="right">
<ArrowRight />
</Button.Icon>
<Button.Label>Continue</Button.Label>
</Button>
Composition makes each piece simple and testable. Configuration creates combinatorial explosion.
The Rule of Three
Don't abstract a pattern into a component until you've seen it three times. The first instance is a one-off. The second is a coincidence. The third is a pattern. Premature abstraction creates components nobody uses and everyone maintains.
Prop Contracts
Every prop should have a clear type, a clear default, and a clear behavior. Ambiguity in props creates ambiguity in rendering:
- Use TypeScript interfaces — Props are documented by their types, not comments
- Required over optional — Force callers to be explicit about intent
- Discriminated unions — Make invalid states unrepresentable
- No boolean props —
variant="primary"is clearer thanprimary={true}
Versioning Strategy
A design system without versioning is a design system without trust. Teams won't adopt a system if every update might break their product.
Semantic Versioning
We follow semver strictly:
- Major (x.0.0) — Breaking changes to component APIs, token removals, behavior changes
- Minor (0.x.0) — New components, new props, new tokens — all backward-compatible
- Patch (0.0.x) — Bug fixes, documentation updates, non-functional improvements
Migration Guides
Every major version ships with a migration guide and automated codemods. The codemods handle the mechanical changes — renaming props, updating imports, adjusting API signatures. The migration guide explains the "why" behind each change.
Deprecation Timeline
When we need to remove a component or prop, we follow a strict deprecation cycle:
- Mark deprecated — Console warnings, documentation notices, 3-month window
- Ship codemod — Automated migration tool available before removal
- Remove in next major — Never in a minor or patch release
Documentation That Works
Documentation must be embedded in the workflow, not siloed in a separate site. If developers have to leave their IDE to find answers, they won't.
In-Editor Documentation
Every component ships with JSDoc comments that surface in IDE tooltips. Hover over a component, see its props, see its defaults, see a brief usage example. No context-switching required.
Living Documentation
Documentation is generated from code, not written separately. Storybook stories serve as both documentation and visual regression tests. Every prop combination is demonstrated. Every edge case is documented.
Decision Records
When we make a significant design decision — why we chose a particular component API, why we deprecated a pattern, why we structured tokens a certain way — we write an Architecture Decision Record (ADR). These records prevent revisiting settled decisions and help new team members understand the "why" behind the system.
Conclusion
A design system that survives growth isn't one that's perfectly designed from the start. It's one that's designed to evolve. Token architecture that supports rebranding. Component APIs that compose rather than configure. Versioning that builds trust. Documentation that's embedded in the workflow.
The structural choices you make early — how tokens are named, how components are API'd, how changes are versioned — determine whether your system scales or shatters. Get the structure right, and the system grows with your product. Get it wrong, and you're rebuilding in 18 months.
Build for evolution, not perfection. The system that survives is the one that can change.