Frontend Architecture
Frontend architecture is the discipline of structuring UI code so it stays fast and maintainable as teams and features grow: design systems for shared visual language, component architecture for composability, and module federation plus monorepos for shipping independent frontends. Done well, the frontend scales like a product instead of a pile of components.
What is this?
Frontend architecture covers how UI code is organized and delivered: a design system provides the shared visual language (tokens, components, docs); component architecture defines how components compose, own state and communicate; and module federation or a monorepo decides how teams ship and version their slices of the interface. It is the frontend answer to the same scalability questions the backend answers with services and boundaries.
Why do you need it?
- A design system centralizes visual decisions so twenty screens cannot drift into twenty shades of blue.
- Well-defined component boundaries make UI behavior testable and reusable instead of copy-pasted.
- Monorepos make shared code, atomic changes and cached CI for many frontends tractable.
- Module federation lets independent teams deploy UI slices on their own cadence without a coordinated release.
- Good frontend architecture keeps bundle size, render performance and developer velocity predictable as the app grows.
Mental model
A city with no building code
A city with a shared architectural standard
Every building still belongs to a different owner, but shared standards — streets, zoning, facade rules — keep the city coherent. A design system is the frontend's zoning code: individual apps remain independent while the shared standards keep them consistent and interoperable.
How it works
- A design system publishes tokens (color, spacing, typography) and presentational components with a versioned API, so product teams consume rather than reinvent the visual language.
- Component architecture separates containers (they know data and state) from presentational components (they know markup and styles), keeping components composable and predictable.
- State stays close to where it is used and is lifted or shared only when siblings truly need it; providers and hooks make cross-cutting concerns explicit.
- A monorepo hosts multiple frontends plus the design system, so shared code is versioned atomically with its consumers and CI builds only what changed.
- Module federation lets a host app load remotes — separately built and deployed frontends — that expose components over the network, each released on its own cadence.
From a shared design system to independently deployed apps that compose at runtime.
Key commands
npx create-mf-app --name remote --type remote --framework reactScaffold a module federation remote application.
pnpm --filter @acme/design-system buildBuild just the design-system package inside the monorepo.
@acme/design-system:build: done in 1.2s
npx storybook dev --port 6006Run the component playground to develop components in isolation.
npx webpack serve --config remote/webpack.config.jsServe the remote app that exposes components at runtime.
npx nx affected -t testTest only the projects affected by changed files in the monorepo.
node scripts/sync-tokens.mjsGenerate token files from the design export so code and design stay in sync.
Examples
A presentational Button styled from design-system tokens
// design-system/src/Button.tsximport { tokens } from "./tokens" export type ButtonVariant = "primary" | "secondary" | "ghost" interface ButtonProps { variant?: ButtonVariant disabled?: boolean children: React.ReactNode} const styles: Record<ButtonVariant, string> = { primary: tokens.bg.accent + " " + tokens.text.inverse, secondary: tokens.border.default + " " + tokens.text.default, ghost: tokens.text.muted,} export function Button({ variant = "primary", disabled = false, children }: ButtonProps) { return ( <button className={styles[variant]} disabled={disabled} data-variant={variant}> {children} </button> )}Button is presentational: it takes props, renders markup and knows nothing about data or state. Its look comes entirely from design-system tokens, so a rebrand touches one file. Consumers compose it with their own data flow instead of editing global styles per screen.
In production
- A large retailer splits its storefront into host plus catalog and checkout remotes, each owned by a different team and deployed weekly.
- A versioned design-system package is consumed by six product teams; a token change propagates to all of them in one release.
- A monorepo with CI caching rebuilds and tests only affected frontends, cutting a full rebuild from minutes to seconds.
- Storybook acts as living documentation: engineers verify every visual state of a component before it ships.
Best practices
- Keep presentational components in the design system and containers (data + state) in the app that owns the feature.
- Version tokens and components semantically and document breaking changes before deprecating anything.
- Use a single state source of truth per feature and derive UI state from it instead of mirroring state in props.
- Treat bundle size as a budget: lazy-load routes and remotes, and keep shared dependencies version-aligned in the monorepo.
- Make components accessible by default: keyboard, focus and screen-reader behavior are part of the API.
Common mistakes
- Building a 'design system' that is really a component dump with no tokens, docs or versioning discipline.
- Putting data fetching and business logic inside presentational components, making them impossible to reuse.
- Copying components between apps instead of publishing them, so the same widget drifts in five places.
- Composing module federation remotes that share large duplicated dependency bundles, bloating the host download.
Common issues
Invalid hook call — react is duplicatedHost and remote each load their own copy of React instead of sharing it via federation.
Add react and react-dom to shared in the module federation config so both sides use the same instance.
Remote module failed to load (TypeError: Failed to fetch)The host could not reach the remote's remoteEntry.js, often a version mismatch or the remote not running.
Serve the remote app, verify remoteEntry.js returns 200, and align the federation config versions.
Design styles missing in the hostThe remote's CSS was not extracted, or tokens are scoped to a namespace the host does not load.
Extract remote CSS in the build and ensure token files are imported before component modules run.
Debugging tips
- Reproduce the bug in Storybook first: if it fails there, the issue is the component; if it passes, it is the container or data.
- Check the shared-dependency map when a federation remote misbehaves — duplicated React instances break hooks and context.
- Inspect token drift by searching for hard-coded hex colors outside the design-system package.
- Profile with the browser's coverage and network panels to find which route or remote adds the most bytes.
Interview checkpoint
Hands-on lab
- Create a monorepo workspace with a shared design-system package
- Build a presentational Button component from design tokens
- Consume the design system from two independent apps
- Verify the shared package resolves identically from both apps
Compose a Design System Across Two Apps
45 min · intermediate
Quick quiz
What is the main difference between a design system and a component library?
In module federation, the host and the remote must ship as a single combined deployable.
Three apps hard-code the same shade of indigo, and the brand changes the color. What is the correct fix?
Cheat sheet
TokensSingle source for color, spacing, typePresentational componentsNo data, no state — pure propsStorybookLiving docs for every component stateSemantic versioningBreaking changes documentedMonorepoShared code, atomic changes, cached CIModule federationRuntime composition of remotesLazy routesCode-split by route or featureShared singletonsOne React instance across remotes