Layered vs Feature-Sliced: Two Ways to Cut the Same App

Take any non-trivial React app and you can describe its whole folder structure as the answer to one question: when you cut this codebase into pieces, which axis do you cut along? There are really only two honest answers, and almost every architecture discussion I've ever sat in is secretly an argument about which one a team picked without realizing they'd picked one.
Layered cuts by technical kind. All the components live together, all the hooks live together, all the API calls live together, regardless of which feature they belong to.
src/ components/ hooks/ api/ utils/ pages/
Feature-sliced cuts by product concern. Everything a feature needs — its components, its hooks, its API calls — lives together, and it's the kind of file that becomes secondary.
src/ features/ payment-method/ order-history/ account-settings/
Neither of these is a mistake. They're both legitimate answers to "how do I organize this," and the reason this keeps being a live debate is that each one is correct for a while and then becomes expensive in a completely different, entirely predictable way.
Every architecture is a bet about which kind of future change you're optimizing for. Layered bets on stability of kind. Feature-sliced bets on stability of feature boundaries. Neither bet is safe forever.
What layered actually gives you
Layered architecture is the one nobody has to be taught. A junior developer can guess where a new hook goes — hooks/ — without reading a single doc. That's not a small thing. It's the lowest-friction structure there is for the first several months of a project, precisely because it doesn't ask anyone to make a judgment call about feature boundaries before those boundaries are even clear yet.
It's also genuinely good for a certain kind of change: anything that's cross-cutting by nature. If you're auditing every place the app calls the API layer, or refactoring every hook to a new data-fetching pattern, layered structure puts all of that in one folder, and you can see the whole blast radius by opening one directory.
Where it breaks is exactly where products stop being simple. Once a feature — say, an order-management flow — starts touching a dozen components, several hooks, a couple of API modules, and its own utils, "layered" stops meaning "organized" and starts meaning "this feature's logic is now distributed across five folders that also contain twenty other features' logic." Want to delete the order-management feature? You're now grepping across the whole codebase, hoping you find every file that only exists because of it. Layered structure doesn't track feature boundaries, so nothing in the folder tree tells you where a feature ends.
What feature-sliced actually gives you
Feature-sliced structure solves exactly that problem. A feature's folder is a feature's blast radius. Deleting features/order-history/ deletes the feature — not perfectly, there's usually some shared code left behind, but close. Onboarding someone onto one feature means pointing at one folder, not explaining the layer system and hoping they can mentally reconstruct which fragments across five layers belong to the thing they're working on.
It scales with team size in a way layered structure doesn't. Multiple teams, each owning a handful of features, can work with almost no folder-level collision, because they're rarely in each other's slices. That's the real argument for feature-sliced at scale — not "it's cleaner," but "it lets N teams work without stepping on each other's files."
The cost shows up the moment two features need the same thing. Every feature-sliced codebase eventually accumulates a shared/ folder, and that folder has exactly the failure mode you'd expect: things get promoted to it reactively, usually the first time a second feature needs something, rarely with much thought about whether it's actually a stable, general-purpose piece or just two features that happen to look similar today. Left unmanaged, shared/ becomes the layered architecture's utils/ folder wearing a nicer name — a dumping ground, just one level removed.
And there's a subtler cost: feature-sliced structure asks you to correctly guess feature boundaries before the product has told you what they are. Early in a project, "is this one feature or two" is often genuinely unknowable, and picking wrong means a slice that either merges awkwardly with another later or splits painfully in two.
The actual decision
I don't think there's a universal right answer here, and I'm suspicious of anyone who gives you one without asking what you're building. The question that actually matters is: what changes more often in this product — the technical kind of thing, or the feature it belongs to?
If you're building something where features are genuinely stable and rarely added (an internal admin tool with five screens that haven't changed in two years), layered structure's simplicity is worth more than feature-sliced's isolation, because you're not paying feature-sliced's up-front cost of guessing boundaries for a product that isn't going to grow new ones.
If you're building something where the number of features is the thing that grows — a product adding new user-facing capabilities every quarter, with multiple teams shipping in parallel — feature-sliced earns its cost almost immediately, because the layered alternative means every new feature adds friction to five existing folders instead of creating one clean new one.
Most real products start in the first category and migrate into the second without anyone deciding to. That's the actual argument for feature-sliced conventions like FSD: not that they're objectively better, but that the underlying problem they solve — coordination at scale — is the one almost every successful product eventually has, even if it didn't start with it.
A hybrid is not cheating
The reasonable middle ground, and the one I actually reach for on real projects, is layered at the top for genuinely global concerns — routing, design tokens, a handful of app-wide providers — and feature-sliced underneath for everything that's actually product surface area.
src/ app/ // routing, providers, global config — layered, rarely touched shared/ // genuinely cross-feature primitives, promoted deliberately features/ payment-method/ // feature-sliced from here down order-history/ account-settings/
The discipline that makes this work isn't the folder names. It's being honest about which things are actually global (rare, and worth naming explicitly) versus which things just look similar between two features today and might diverge next quarter. Promote to shared/ when a third feature needs something, not when a second one does — that one rule prevents most of the premature abstraction that turns shared/ into a second utils/ folder.
Pick the axis that matches what actually changes in your product, be honest about which folder is doing that job right now, and don't confuse "we chose a structure" with "we solved organization." You didn't solve it. You picked which future problem you'd rather have.
More than a blog post
I share frontend news and the reasoning behind it throughout the day. Pick the language that feels natural to you.