Topic · 4 articles

Frontend architecture

The architectural decisions that decide whether a frontend codebase ages well — component boundaries, state ownership, server/client splits, and the data-fetching patterns that hold up at scale.

Frontend architecture is the topic where the cost of getting decisions wrong is the most expensive to undo. A poorly-chosen state management library survives in a codebase for years after the team realises it was the wrong fit. Component boundaries that made sense in the first month become the reason every feature requires touching nine files. The server/client split that felt natural during the initial build becomes the reason every new page is slower than the last. This topic is about the small set of architectural decisions that compound across the life of a frontend.

Posts split into four areas. The first is component design — the boundaries, the props shape, the composition patterns that make a UI library actually reusable rather than a graveyard of half-shared components. The second is state ownership — where data lives, who is allowed to mutate it, and how the layers communicate without becoming entangled. The third is the server/client split that modern frameworks have made central — what runs where, what hydrates, what stays on the server permanently. The fourth is data fetching — the patterns for loading, caching, invalidating, and revalidating data that hold up when the app grows past a handful of pages.

If you are new to the area, start with the component-boundaries post and the state-ownership guide. If you are working on a Next.js or Remix codebase, the server/client deep-dive is the natural follow-up. The data-fetching post is the one that pays back most over time on any codebase that grows past a single team.

Covers:Component boundaries and composition over configurationLocal vs global state — which lives whereServer components, client components, and the boundary between themData fetching, caching, and revalidation patternsThe cost of context, providers, and prop-drilling
📖 Topic guide

Frontend architecture: the decisions that compound and the ones that don't

Architectural decisions at the frontend layer are unusually sticky. Most of them survive in the codebase long after the team that made them has moved on. This guide is about the small handful of decisions that genuinely compound — that make every future change easier or harder depending on the choice — and how to recognise them when you are making them.

01

Which decisions actually compound

Most things in a frontend codebase are reversible cheaply. A button's styling can be changed in an afternoon. A route can be renamed in an hour. A library can be swapped for a competitor in a sprint. The decisions that compound are the ones that touch the shape of the data that flows through the app — where state lives, what shape it takes, and which layer is allowed to mutate it.

The reason those decisions are sticky is that every component, every page, every feature gets written against the existing data shape. Once a Redux store, a React Context, or a TanStack Query cache has been the source of truth for three months, hundreds of components depend on its specific API. Changing the shape means touching every one of those call sites. Most teams never do.

The corollary is that the cost of getting the data layer wrong is the cost of working around it forever. Teams that picked the wrong state library in year one spend year two and year three building escape hatches — local state that should have been global, global state that should have been local, custom hooks that wrap the library to make it usable. The team's productivity is measurably worse than a team that picked a library that fit the shape of the app from the start, and the difference compounds across every new feature.

02

Component boundaries are the first architecture

Before any framework or library decision, a frontend codebase makes a series of decisions about where one component ends and another begins. Most of those decisions are made during the first month of the project, by whoever happens to be writing the first few screens, and they shape every later contribution. A codebase where buttons take 18 props because the original author tried to handle every possible variant is a codebase where every new variant adds prop #19. A codebase that composes small primitives instead is a codebase where the same variation is a five-line wrapper at the call site.

The discipline most experienced teams converge on is composition over configuration. A component takes a small number of props that describe what it is, not how it should look in every conceivable context. Variants are built by composing smaller components — slot props, render props, children-as-functions, headless component libraries — rather than by piling configuration onto a single component. The result is a UI layer where new variants do not require touching old code, and where the cost of adding a feature is proportional to the feature's size, not to the codebase's size.

The component-boundaries post in this topic walks through the specific moves: how to spot a component that has accumulated too many props, the refactoring path from a configuration-heavy primitive to a composition-heavy one, and the small set of design tokens that should be passed down rather than re-declared on every component.

03

State ownership: which layer holds what

The single most common architectural mistake on growing frontend codebases is putting state in the wrong layer. Form inputs end up in global state because someone thought "the rest of the app might need them"; server data lives in component state because nobody set up a cache; transient UI state — which dropdown is open, which row is selected — lives in a Redux store because that was the team's default for everything.

The state-ownership discipline is small. Server data — anything the backend owns — lives in a dedicated server-state library (TanStack Query, SWR, RTK Query, or whatever the framework provides). Client state that is shared across pages — auth, theme, feature flags — lives in a small global store. Client state that belongs to a single component or its immediate children lives in that component's local state. The three layers do not bleed into each other.

Getting this split right at the start removes a category of bug that otherwise haunts the codebase. Stale server data because someone is hand-managing a Redux slice when the cache library would have invalidated correctly. Form inputs that survive across navigations because they were elevated to global state. Modals that don't reset because their open-state was treated as global. The state-ownership post in this topic covers the specific rules each layer should follow, with the refactoring paths to fix codebases that have drifted.

04

The server/client split is the new architecture

Modern frameworks — Next.js App Router, Remix, SvelteKit — have made the server/client boundary the most consequential single architectural decision in a frontend project. Components run on the server unless they explicitly opt into the client. Data fetching happens server-side by default. The JavaScript bundle that the browser receives is shaped entirely by which components were marked as client.

The compounding decision here is not which framework you use; it is the discipline of keeping client components small. A page where everything is marked client is a page that ships a large JavaScript bundle for no reason. A page where only the genuinely interactive elements are client components — the form, the dropdown, the chart — is a page that loads quickly and hydrates cheaply.

The deep-dive on server/client patterns covers the recurring decisions: when to pass server data through a client component versus fetching it again on the client, when context providers need to be client components, and how to handle the situation where a small piece of interactive state lives inside an otherwise-static page. None of those decisions are obvious in the abstract; they only make sense once you have seen the patterns repeat across enough projects.

05

Data fetching is the layer that grows the fastest

On most frontend codebases, the data-fetching layer is the part of the architecture that most needs to be deliberate, and the part that most often gets handled ad-hoc. The first version is usually a useEffect with a fetch call. The second version is a wrapped fetch with a loading state and an error state. By the time the team realises the pattern has been copy-pasted into 60 components, the costs of standardising on a real data layer have been paid in scattered bugs.

The right move is to pick a data-fetching library on day one and route every server interaction through it. The library handles caching, invalidation, request deduplication, retries, optimistic updates, and the loading/error state machines that every component otherwise reinvents. The team writes hooks that describe what data they want; the library handles when and how to fetch it. The savings compound across every new feature.

The patterns post in this topic covers the four that show up most often: simple queries with a stable key, paginated lists that need infinite-scroll handling, mutations with optimistic updates, and the cache-invalidation rules that connect mutations back to the queries they affect. None of those are framework-specific. All of them are small, learnable patterns that make a codebase orders of magnitude easier to maintain.

Frontend architecture is not glamorous work. Most of it is small decisions, repeated consistently across the lifetime of a codebase, that decide whether the next feature takes a day or a week. The teams that take it seriously do not write more clever code than the teams that do not. They write code that is easier to read three months later. That is the whole compounding effect, and it is the thread the posts in this topic are written around.

Articles in this topic

4 posts

Want a wider view?

See every topic on one page, plus the writers behind them.

All topics →