Skip to main content
shivam gairola..

OPEN TO SELECT PROJECTS

OPEN TO SELECT PROJECTS

OPEN TO SELECT PROJECTS

OPEN TO SELECT PROJECTS

Frontend

Frontend Architecture That Survives a Growing Team

The hardest part of a large frontend is not the framework, it is the boundaries. How I structure apps so they stay easy to change as the team and the codebase grow.

Jul 2, 20265 min read

Most frontend codebases do not die from a bad framework choice. They die from erosion. Every feature adds a little coupling, every deadline skips a little structure, and eighteen months later a one-line change touches nine files and nobody is sure why. The framework was never the problem. The boundaries were.

Architecture is the set of decisions that are expensive to reverse. In a frontend that means where state lives, how modules depend on each other, and what a "feature" is allowed to reach into. Get those right and the framework becomes a detail you could swap. Get them wrong and no amount of React or Vue expertise saves you.

Here is how I think about it after doing this on a few large apps.

Organize by feature, not by file type

The default folder structure most tutorials teach groups files by what they are: all components in one folder, all hooks in another, all services in a third. It looks tidy on day one and becomes a nightmare by month six, because a single feature is now smeared across five top-level folders and you scroll endlessly to make one change.

Group by feature instead. Everything a feature needs lives together.

src/
  features/
    checkout/
      components/
      hooks/
      api/
      checkout.store.ts
      index.ts        <- the ONLY public entry point
    catalog/
    account/
  shared/             <- genuinely cross-feature building blocks
    ui/
    lib/
  app/                <- routing, providers, composition root

The rule that makes this work is the barrel file. A feature exports a deliberate public surface through its index.ts, and nothing outside the feature is allowed to import from its internals. If checkout needs something from catalog, it imports from catalog, not from catalog/hooks/useSomethingPrivate. This one constraint prevents the slow slide into a big ball of mud.

Draw the dependency direction and never break it

The single most useful diagram for a frontend is the allowed direction of dependencies. Mine almost always looks like this:

   app  (routing, providers, wiring)
    |
    v
  features  (checkout, catalog, account)
    |
    v
  shared  (ui kit, utils, api client)

  Dependencies point DOWN only.
  - features may use shared
  - features must NOT import each other directly
  - shared must NEVER import a feature

The moment shared imports from a feature, or two features import each other directly, the graph has a cycle and your ability to reason about, test, and delete code collapses. Cross-feature communication should go through the app layer, an event bus, or shared state, not direct imports.

You can enforce this mechanically. ESLint rules like import/no-restricted-paths or a boundaries plugin will fail the build when someone reaches across a line they should not. Architecture that is only a diagram in a wiki gets ignored. Architecture that fails CI gets respected.

Put state where it belongs, not where it is convenient

State is where most frontend architectures rot, because reaching for one global store for everything feels easy. Separate state by its actual nature:

  • Server state is data you fetched and do not own. It has caching, revalidation, and staleness concerns. Use a tool built for it (TanStack Query, RTK Query, SWR). Do not hand-roll this into a global store.
  • Client state is UI you own: a modal being open, a form's draft values, a selected tab. Keep it as local as possible. Colocate it with the component that uses it and only lift it when something above genuinely needs it.
  • URL state is anything that should survive a refresh or be shareable: filters, pagination, the active view. The URL is a store too, and it is the most underused one.

The mistake I see most is dumping server data into a global client store and then fighting cache invalidation by hand forever. Match the tool to the kind of state and half your "state management is hard" problems disappear.

Isolate the framework at the edges

You will not rewrite React tomorrow, but you should be able to. The way you protect yourself is to keep framework-specific and vendor-specific code at the edges, and keep your core logic plain.

Business rules belong in plain functions you can test without rendering anything. Data fetching belongs behind a thin client so swapping the transport does not ripple through every component. Third-party SDKs, the analytics tag, the feature-flag client, the payment widget, all get wrapped in an adapter you own. When the vendor changes their API, you change one file, not fifty.

This is not over-engineering. It is the difference between a vendor migration being a sprint and being a quarter.

Make the composition root explicit

Somewhere your app wires everything together: providers, the router, the query client, the theme, error boundaries. Make that one obvious place, the composition root, and keep it thin. Features should not each reach out and initialize global concerns. They declare what they need, and the root provides it.

When a new engineer asks "where does the app actually start and what is set up globally", there should be a single, boring answer.

The payoff

None of this is about being clever. It is about making the codebase boring in the specific way that lets a team move fast without stepping on each other. Feature-based boundaries mean two people can work in parallel without conflicts. A one-directional dependency graph means you can delete a feature without archaeology. State placed by its nature means fewer bugs at the seams. Framework isolation means today's default is not tomorrow's prison.

The test of good frontend architecture is simple: can a new engineer ship a real feature in their first week without needing to understand the whole system, and can you delete a feature without fear. If both are true, the boundaries are doing their job.

Building something in this space?

I take on select builds when the work is worth doing right.

Start a conversation