I used to love the word "architecture." I'd put it on slide decks. I'd argue about it on PR reviews. I'd lobby — sometimes aggressively — for the version of the layout I'd seen on a conference talk that month. I had opinions about folders. I had opinions about which layer owned which concern. I'd talk for half an hour at a whiteboard about whether something belonged in features or domain or entities.
I no longer trust most of what I used to say.
It's not that I think frontend architecture doesn't exist. It's that the word has become a kind of shorthand for whatever the person using it wants to do anyway, and we've all agreed not to notice. Somebody pushes for a monorepo because they read about Turborepo and want to try it; they call it "architecture." Somebody pushes for a hexagonal-ish layered structure with use cases and repositories and adapters; they call it "architecture." Somebody pushes for a features folder over a pages folder; they call it "architecture." None of these things are exactly wrong. But they're not architecture either. They're folder structure, build configuration, and code organization, all dressed up in a word that implies more rigor than they actually contain.
This post is mostly about what I now think does count, and why I'm more skeptical of the rest.
What I actually look at now when I open a codebase
When I join a new team and open their main repo for the first time, I no longer judge it by the folder structure. I used to. I'd see src/components, src/pages, src/utils and roll my eyes. I'd see src/features/billing, src/features/onboarding, src/features/dashboard and feel reassured. None of that judgment was worth anything. I've worked in lovely "feature-folder" codebases that were unmaintainable and lovely "everything in components" codebases that were a joy.
Now I look at four things, in roughly this order.
Can I tell where to add a new feature without asking? This is the first one and it's by far the most important. If I'm onboarding and the team gives me a small new screen to build, can I figure out where it goes by reading the existing code? Can I do it without DM-ing someone? If yes, the codebase has good legibility, and legibility is what people actually mean when they say architecture is good. If no, something is wrong, and no number of conference-approved patterns will fix it. A codebase where the answer is obvious is a codebase that's going to age well. A codebase where the answer requires a half-hour conversation is one that's already accumulating debt the new contributors don't know to look for.
Is the boundary between framework code and product code visible? This one I learned the hard way. Every frontend codebase has two kinds of code in it. There's the code that exists to do React or Vue things — providers, custom hooks/composables, generic wrappers, types for the routing layer. And there's the code that exists to model the actual product — a customer, a subscription, a chart of weekly active users. In a healthy codebase, you can tell which is which at a glance. In an unhealthy one, the product logic gets tangled into the framework code, the framework code starts modeling product concepts, and nobody can move anything without breaking something else. Where this boundary lives is more important than what it's called.
Is shared state genuinely shared, or just conveniently global? I'll come back to this one because it's where most of my regret lives.
Does the team understand the build pipeline, or fear it? A frontend codebase is, in a real sense, the artifacts produced by its build. If the build is a magical black box that one person on the team set up three years ago and nobody else has touched, that's a structural problem dressed up as a tooling problem. The team's relationship with its own toolchain is part of the architecture, even though nobody puts it on the architecture slide.
That's the list. Folder structure isn't on it. Whether you use features/ or pages/ or modules/ is, I have come to believe, almost completely irrelevant. The codebases where one of these works are the ones where the conventions are consistent and the team agrees on them. The actual name of the top-level folder is a coin flip.
What I championed in 2022 that I now think was wrong
I want to be specific here because vague self-criticism is a way of avoiding actual self-criticism.
Global stores for everything. Somewhere around 2021–2022 I went through a phase of putting a lot of state into Redux (and later Zustand and Pinia, when I switched between projects). Form state, modal open/closed state, hover state on a list item, you name it. The reasoning was that having "one place to look for state" was good, and so the more state I centralized the better. This was wrong. Putting transient UI state into a global store creates coupling between components that have no business knowing about each other, makes every state change a global concern, and turns dev tools that should be useful (the Redux/Pinia inspector) into firehoses you have to scroll past to find anything. The state that belongs in a global store is server data, cross-cutting flags like auth and theme, and a small amount of explicitly shared business state. Everything else is better off local, and "I might need this in another component someday" is not, it turns out, an actual reason to globalize anything.
Premature monorepos. I led a project where we split a working application into four packages in a Nx monorepo because we anticipated that one of them might eventually be reused in a different app. The other app never got built. We spent the next year paying the tax: package boundaries we had to maintain, circular dependency errors, build orchestration that broke whenever someone updated a dep, IDE indexing that got slower with every package added. If the second consumer had materialized, the monorepo would have been the right call. It didn't, and the monorepo was an enormous cost for hypothetical reuse. I now think monorepos make sense when you already have two real consumers. Not before.
Hexagonal-ish architecture in CRUD apps. I worked on, and partly designed, a frontend codebase where every API call went through a "use case" layer that called a "repository" interface that was implemented by an "adapter" that ultimately called fetch. The shape was lifted from clean architecture writeups. It was beautiful on the whiteboard. In practice, adding a new endpoint required touching five files, the abstractions did nothing for testing because we mocked at the network layer anyway, and the only person who fully understood the layering was me. Six months after I left that team, somebody mercifully simplified all of it down to thin API clients. It was the right call. I'd have made the same call eventually. The lesson is that layered architectures pay for themselves when the layers each have multiple implementations or genuinely vary. A CRUD app where every "use case" calls one endpoint is not that. I confused architectural rigor for actual value.
Generic form abstractions before there were three forms. This is the one I'm most embarrassed about. We had two forms in our app. I built a <Form> wrapper that handled validation, error display, submission state, optimistic updates, and a half-dozen edge cases. The next time we had to build a form, it didn't fit the abstraction — the validation timing was different, the submission flow was wrong, the optimistic-update shape was incompatible. So we extended the abstraction. The extension broke the original two forms. We patched the original two. By the time we had four forms in production, the wrapper component was over 500 lines, had a configuration object with eighteen options, and any change to it required reading every one of its callers to know if you'd broken something. We eventually ripped it out and wrote each form as its own small thing. They share a few utility functions. That's it.
The pattern in all four of these mistakes is the same: I optimized for an imagined future that didn't arrive, and I paid the cost in real present-day complexity. Premature generalization is the most expensive thing I've consistently done as a frontend engineer, and the part that's particularly insidious is that it always looks, in the moment, like the senior thing to do.
Shared state is the actual architecture problem
I want to come back to the shared state question because, having now worked on enough frontend codebases, I think this is the part where the word "architecture" still earns its keep. Almost everything else can be reorganized later. Shared state can't, easily.
When two components need access to the same piece of state, you have a real architectural decision to make. You can lift it up. You can put it in context (in React) or provide/inject (in Vue) or a store. You can pass it through props. You can read it from a query cache. Each of these has consequences that compound: lifting state up creates a parent that knows about both children's concerns; putting it in a global store makes every change observable to every consumer, which is great for some things and disastrous for others; passing through props is fine until your tree is deep enough that you have prop-drilling fatigue and someone reaches for context anyway.
The good codebases I've worked in handled shared state with a kind of careful prejudice: things that are genuinely shared (the current user, the theme, the active workspace) live in dedicated stores or contexts that are obvious; things that are coincidentally shared (two components that both happen to need the current row's data) are passed explicitly through props or pulled from a query cache, not promoted to global. The bad codebases I've worked in treated the difference as a matter of style. They paid for it for years.
This, more than the folder structure, more than the build setup, more than any of it, is what determines whether a frontend codebase ages well. State is the part of the system that's hardest to refactor later. Everything else is text in a file.
Where this leaves me
I don't have a tidy framework to recommend at the end of this. Honestly, the realization that I don't is part of the point of this post.
If you'd asked me four years ago what good frontend architecture looked like, I'd have drawn you a diagram. Today I'd ask you a list of questions about your team. How many developers? How fast are they shipping? Is the current code legible to people who didn't write it? Where does state live, and is that where it should live? Do you have an explicit shared component layer, and is the boundary holding? Are the conventions consistent, even if they're not the conventions I'd have picked? Most of the answers people give me to these questions tell me far more about whether their codebase is in trouble than any diagram could.
The other shift, the one I haven't quite known how to write about until now, is that I've become much more suspicious of architectural confidence in general. The patterns I was most certain about at different points in my career are the same patterns I later regretted. The people I respect most professionally are the ones who hold their architectural preferences loosely, who refactor toward problems rather than toward ideals, and who are comfortable saying "I don't know, let's start simple and see what hurts."
That last phrase — let's start simple and see what hurts — is, I think, doing more work in a healthy frontend codebase than any architecture document ever does. It's certainly doing more for the codebases I'm proud of than the diagrams I used to draw.