Crafting a resilient design system

How to organize and scale UI components using atomic patterns, package architecture, and comprehensive documentation

Example of page from Love.irish's design system

As a fullstack product engineer, I've learned that the difference between a design system that thrives and one that becomes technical debt lies not just in the components themselves, but in how they're organized, documented, and evolved. Over the past year building Love.irish — a Celtic language learning platform — I've developed a design system with 55+ packages that demonstrates how atomic design principles combined with thoughtful tooling can create a truly resilient foundation.

The Challenge of Scale

When you're building a complex application, UI consistency becomes increasingly difficult to maintain. Components get duplicated, design patterns diverge, and teams struggle to find existing solutions. The traditional approach of a monolithic component library often breaks down because:

  • Components become tightly coupled and hard to modify
  • It's unclear which components to use for specific use cases
  • Documentation becomes stale and disconnected from implementation
  • Different domains (marketing, product, admin) have conflicting needs

The solution isn't just more components — it's better organization.

Foundation: Atomic Design Architecture

The atomic design methodology provides the perfect framework for thinking about component hierarchies. Rather than arbitrary categorization, it mirrors how we naturally think about interface composition:

Preview & lofi components

These are a special type of component in the app which represents the lofi version of a component or template. They are built with Tailwind and JSX to ensure colors, spacing and other design tokens are consistent with the rest of the app. They are used in Storybook to provide a quick preview of how a component or template will look without needing to run the full application.

Each pattern has a preview component which makes it easy to use around the site for in prototyping. An example of this is the profile Lofi component used in the app or the thumbnail previews used in the Storybook design system.

Example of elements like avatar, badge and button
Example of the profile Lofi component used throughout the Love.irish app like on the rewards page

At the foundation are your most basic, reusable elements. In Love.irish, this includes components like:

import ProfileLofiComponent from "@love-irish/community-lofi-profile-component"
import banner from './banner.png'

<ProfileLofiComponent
  banner={banner}
/>

Atoms: The Building Blocks

Example of elements like avatar, badge and button
Example of elements like avatar, badge and button

At the foundation are your most basic, reusable elements. In Love.irish, this includes components like:

import Button from '@love-irish/button'

// Supports variants, sizes, colors, and states
<Button variant="primary" size="lg" loading>
  Start Learning Irish
</Button>

These atoms are completely context-free and highly configurable. Our button component supports multiple variants (primary, secondary, outline), sizes (sm, md, lg), colors, loading states, and can even render as links. The key is that atoms never assume their context.

Molecules: Purposeful Combinations

Example of elements like avatar, badge and button
Example of a basic formatted resource list item

Molecules combine atoms to create functional units:

// @love-irish/resource-list-item
import ResourceListItem from '@love-irish/resource-list-item'
import addToCollectionMutation from "./add_to_collection.graphql"

<ResourceListItem
  type="codex_record"
  title="Tánaiste"
  subtitle="Deputy Prime Minister"
  onAddToCollection={addToCollectionMutation}
/>

Our ResourceListItem molecule combines buttons, typography, and icons to create different presentations for basic resources, vocabulary words (codex records), interviews, and mini-games. Each variation shares the same fundamental structure but adapts to its content type. It also allows for modular customization through props and not being too restrictive with API design.

Some modules also have predefined variations for common use cases like a CodexRecord which is a wrapper for the ResourceListItem component above:

Example of elements like avatar, badge and button
Example codex record using the ResourceListItem variation
import { CodexRecord } from '@love-irish/resource-list-item'

<CodexRecord
  item={item}
  displayAudioInfo={false} // override dynamic UI like hiding the audio icon even if item has audio source
/>

Organisms: Complex Functionality

Example of elements like avatar, badge and button
StoryTeller UI for replaying scripts with interactive steps like asking for a response to continue

Organisms are where business logic lives:

import StoryTeller from '@love-irish/adventure-story-teller'
import storyScript from './script'

// customBlocks = [...]
// handleSpellCast = (...)
// handleLootDrop = (...)

<StoryTeller
  title="The Enchanted Forest"
  script={storyScript}
  blocks={customBlocks}
  onSpellCast={handleSpellCast}
  onLootDrop={handleLootDrop}
/>

The StoryTeller organism manages complex interactive storytelling, combining multiple molecules and atoms to create immersive learning experiences. It handles state management, user interactions, and orchestrates the various UI elements. It can also use custom blocks to extend functionality for specific story types.

Templates: Layout Patterns

Example of elements like avatar, badge and button
Example adventure templates which allow for customization but have distinct logic from eachother

Templates define page-level structures without content:

// @love-irish/adventure-index-template
import AdventureIndexTemplate from '@love-irish/adventure-index-template'

<AdventureIndexTemplate>
  <SpellCastingSection />
  <LootDropSection />
  <GamePreviewSection />
</AdventureIndexTemplate>

Our templates are fully responsive and include multiple viewport stories for testing across devices. They provide consistent layout patterns while remaining flexible enough to accommodate different content.

Namespace Strategy for Scale

One of the most important decisions was how to organize packages. Rather than generic categorization, I chose domain-driven namespaces that reflect how our team thinks about the product:

@love-irish/adventure-*          # Gaming and interactive content
@love-irish/resource-*           # Learning materials and references
@love-irish/application-*        # Core app functionality like footer and layout wrappers
@love-irish/community-*          # Social and user-generated content
@love-irish/*                    # Foundational components and hooks dont have a namespace
@love-irish/ui-components        # Shared UI primitives

Template Naming Convention

Templates get the -template suffix to clearly distinguish them from components:

@love-irish/adventure-index-template
@love-irish/resource-codex-record-template
@love-irish/application-dashboard-template

This convention makes it immediately clear what's a reusable component versus a specific page layout, helping developers make better decisions about what to use and when.

Storybook as the Design System Hub

Storybook serves as both development environment and living documentation. Every package includes comprehensive stories with sensible defaults assigned globally like autodocs to generate an overview page or including responsive layout stories in the default package template.

Example of elements like avatar, badge and button
Daily riddle template built with mostly other design system components and has support for casting spells

This is an example for the Daily riddle template:

export default {
  title: "Adventure/Templates/DailyRiddle",
  component: DailyRiddle,
  argTypes: {
    npcId: {
      options: Object.entries(characters)
        .filter(([id, data]) => data.type === "npc")
        .map(([id, _data]) => id),
      control: {
        type: "select",
      },
    },
    currentSpell: {
      options: Object.entries(spells).map(([id, _data]) => id),
      control: {
        type: "select",
      },
    },
  },
  args: {
    npcId: "npcFairy1",
  },
  tags: [],
  parameters: {
    layout: "fullscreen",
  },
}

const StoryTemplate = (args) => {
  return (
    <div className="min-h-screen">
      <div className="app-container bg-background min-h-screen flex flex-col dark:text-white">
        <AppHeader />
        <section className="grow flex">
          <main className="grow">
            <Template {...args} />
          </main>
        </section>
        <AppFooter />
      </div>
    </div>
  )
}

export const Example = StoryTemplate.bind({})

Example.args = {}

export const WithEmptyState = StoryTemplate.bind({})

WithEmptyState.args = {}

// ... More stories for different states

export const WithTabletView = StoryTemplate.bind({})

WithTabletView.parameters = {
  viewport: {
    defaultViewport: "tablet",
  },
}

export const WithMobileView = StoryTemplate.bind({})

WithMobileView.parameters = {
  viewport: {
    defaultViewport: "mobile2",
  },
}

export const WithMobileLandscapeView = StoryTemplate.bind({})

WithMobileLandscapeView.parameters = {
  viewport: {
    defaultViewport: "mobile2",
    defaultOrientation: "landscape",
  },
}

Key Storybook Patterns

Comprehensive Coverage: 55+ story files across all packages ensure every component is documented and testable.

Responsive Testing: Most components include tablet and mobile viewport stories by default:

export const WithMobileView = StoryTemplate.bind({})
WithMobileView.parameters = {
  viewport: {
    defaultViewport: "mobile2",
  },
}

Interactive Examples: Stories include controls for exploring component behavior and edge cases in isolation.

Automated Documentation: Using autodocs ensures component APIs are always up-to-date with implementation.

Developer Experience Patterns

A resilient design system requires excellent developer experience. Here's how I achieve it:

Yarn Workspaces

// package.json
{
  "workspaces": [
    "packages/*"
  ]
}

Yarn workspaces allow each package to have its own dependencies while sharing common tooling. This enables:

  • Independent versioning of components
  • Isolated logic, testing and development
  • Efficient dependency management and clear separation of concerns
  • Easy cross-package imports

TypeScript Integration

The Love.irish design system uses types from a series of sources including the GraphQL API to make it easy to keep types in sync and reduce errors with trying to use unsupported props or data structures.

# Generate types from GraphQL schema
yarn graphql:types:generate

Then we can use the types throughout the app or use the mutations or queries directly in components:

import { UpdateUserProfileMutation } from "@love-irish/graphql-api"


// ...
const [updateUserProfile, { data, loading, error }] = useMutation(
  UpdateUserProfileMutation
)

All packages are TypeScript-first with automated type generation from our GraphQL API. This ensures type safety across component boundaries and catches integration issues early.

Consistent Package Structure

Every package follows the same structure with a dummy package available to initialize new packages quickly:

packages/[category]-[component-name]/
├── package.json
├── src/
│   ├── README.md
│   ├── index.tsx
│   ├── index.stories.tsx
│   └── index.spec.tsx
└── coverage/ (generated)

This consistency makes it easy for developers to navigate between packages and understand how components are organized.

Resilience Through Organization

The true test of a design system isn't just how well it works today, but how well it adapts to change. Here's how organizations like Love.irish can create resilience:

Isolated Development

Each package can be developed, tested, and versioned independently. This means:

  • Bug fixes don't require coordinating across the entire system
  • New features can be gradually adopted
  • Breaking changes can be managed at the package level
  • Teams can work on different domains simultaneously

Clear Boundaries

The atomic design hierarchy and namespace strategy create clear boundaries:

  • Atoms don't depend on molecules or organisms
  • Domain-specific components are isolated from core functionality
  • Templates are separate from components, reducing coupling

Living Documentation

Storybook serves as executable documentation that's impossible to get out of sync:

  • Every component has interactive examples
  • API changes are immediately visible
  • Visual regression testing catches breaking changes
  • Designers and developers share the same source of truth

Real-World Examples from Love.irish

Here are some more specific examples of how this system works in practice:

Each template encapsulates the unique requirements of its content type while maintaining visual consistency through shared atoms and molecules.

Multi-Language Component Patterns

Irish language learning requires special handling for pronunciation, etymology, and cultural context:

Example of elements like avatar, badge and button
Example of the ogham text component used to render guestbook signatures
import OghamScript from "@love-irish/adventure-ogham-script"

<OghamScript text="grá" />

Components like OghamScript handle the complexity of rendering ancient Irish scripts while providing fallbacks and accessibility features. It also abstracts common logic like preparing the text for transliteration.

Game Component Architecture

Example of elements like avatar, badge and button
Matcher game complete screen that displays summary with confetti

Interactive learning games share common patterns:

import UnscrablerMinigame from '@love-irish/adventure-unscrambler-minigame'
import MatcherMinigame from '@love-irish/adventure-matcher-minigame'

<UnscrablerMinigame
  options={[
    { value: "mise" },
    { value: "_NAME_" },
    { value: "is" }
  ]}
  correctOrder={["is", "mise", "_NAME_"]}
  onCorrect={handleCorrectAnswer}
  onComplete={handleGameComplete}
/>

<MatcherMinigame
  options={[
    { name: "Dia duit", translation: "Hello" },
    { name: "Slán", translation: "Goodbye" },
    { name: "Le do thoil", translation: "Please" }
  ]}
  onCorrect={handleCorrectAnswer}
  onComplete={handleGameComplete}
/>

The shared component patterns make it easy to create new game types while maintaining consistent UX patterns.

Lessons Learned and Best Practices

After building and maintaining this system, here are the key insights:

When to Create New Packages

Create a new package when:

  • The component has a specific domain responsibility
  • It needs independent versioning or deployment
  • Multiple teams will consume it differently
  • It has unique dependencies

Extend existing packages when:

  • The functionality is a variation of existing behavior
  • It shares the same dependencies and lifecycle
  • It's unlikely to be used independently

Balancing Flexibility with Consistency

The biggest challenge is providing enough flexibility without sacrificing consistency. Our approach:

  • Flexible: Allow extensive customization through props and variants
  • Consistent: Enforce design tokens and interaction patterns
  • Documented: Every customization option has stories and examples

Performance Considerations

With 55+ packages, there was a need to be thoughtful about performance:

  • Tree Shaking: Each package exports only what's needed
  • Code Splitting: Templates are loaded on-demand
  • Asset Optimization: Shared assets are extracted to common chunks
  • CSS minification: Tailwind CSS is purged of unused styles and when using libraries can result in shared classes which further reduce compiled asset size

Team Adoption Strategies

Getting teams to adopt a design system requires more than great components:

  • Discoverability: Storybook makes it easy to find existing solutions
  • Documentation: Comprehensive examples reduce implementation friction
  • Migration Path: Old components can coexist with new ones during transitions
  • Feedback Loop: Regular design system office hours and contribution guidelines

Conclusion

Building a resilient design system isn't just about creating components — it's about creating an ecosystem that can evolve with your product and team. The combination of atomic design principles, thoughtful package organization, and comprehensive tooling creates a foundation that gets stronger over time.

The Love.irish design system demonstrates that it's possible to maintain consistency across complex domains while giving teams the flexibility they need to build great user experiences. By treating your design system as a product with its own users (your developers) and carefully considering how components are organized and documented, you can create something that truly scales.

The key insights:

  1. Organization matters more than individual components: atomic design and domain-driven namespaces create intuitive mental models
  2. Documentation must be executable: Storybook ensures your examples never lie
  3. Consistency comes from constraints: design tokens and clear hierarchies eliminate ambiguity
  4. Resilience requires isolation: independent packages reduce coupling and enable gradual evolution

Whether you're starting fresh or refactoring an existing system, these patterns can help you build something that doesn't just work today, but continues to serve your team as your product grows and changes.


Want to explore the Love.irish design system in a real environment? Check out our app to see these patterns in action.