Feature Sliced Design Explained

Picture a frontend project that started small and grew fast. At first, a handful of files held the whole app, and everyone knew where everything lived. A year later, the project holds hundreds of files, the same button logic appears in three places, and a simple change to the checkout flow forces a developer to open a dozen folders just to find the code that matters. Nobody planned this mess. It grew one quick fix at a time, because the project had no rule for where new code should go. Feature sliced design is a set of rules that answers that question before the mess starts, and it keeps answering it as the team grows.
In plain words, this method groups frontend code in two ways at once. First, it groups code by feature, which means a slice of the app that serves one user job, such as signing in, searching products, or paying for an order. Second, it groups code into layers by its job in the app, such as pages that assemble a screen, widgets that combine blocks, features that carry user actions, entities that model business data, and shared tools that many parts reuse. A slice sits inside a layer, and a layer holds many slices. That grid gives every file a clear home, so a developer can guess where code lives before searching for it, and a reviewer can judge a change by looking at one slice instead of the whole codebase.
This guide explains the method in practical terms: what each layer does, how slices work, which import rules keep the structure honest, what benefits teams actually gain, and where the method falls short. It uses examples from ordinary web apps, such as an online store and a dashboard, rather than theory alone. It also covers how to adopt the method in an existing project without a risky rewrite, and which mistakes undo its benefits. If your team builds web apps that keep growing, this guide will help you decide whether this structure fits, and how to use it well if it does. The official overview of the method sets out the same layers and rules in the project's own words, and it is worth reading alongside this guide.
Why Frontend Code Gets Hard to Manage
Frontend projects grow in a particular way. A new screen needs a component, which is a reusable piece of interface, plus logic to fetch data, plus styles, plus a test. Without a placement rule, each of those pieces lands wherever the developer happens to put it. One developer groups by screen, another groups by file type, and a third invents a new folder for a one-off case. After a few months, the folder tree reflects who wrote the code and when, not what the code does. New team members spend their first weeks learning the history of the project instead of learning the product, and even experienced developers hesitate before moving code, because they cannot tell what else depends on it.
The deeper problem is hidden coupling, which means parts of the code depend on each other in ways that are not obvious. A checkout component may import a helper from the user profile code, and the profile code may import a style from the checkout code. Neither import looks harmful alone, but together they create a knot: changing one part breaks the other, and deleting an old feature breaks a live one. Teams feel this as slow reviews, because reviewers must check the whole app for side effects, and as fear of deletion, because nobody knows what is safe to remove. A placement rule alone does not fix coupling, but a placement rule plus import rules makes coupling visible, and visible problems can be fixed.
Grouping code by file type is the most common default, and it shows the problem clearly. A folder called components holds every button, card, and form in the app, while a folder called hooks holds every piece of reusable logic. To change the search feature, a developer opens the components folder, the hooks folder, the styles folder, and the data folder, and edits a little in each. The change is scattered, the review is scattered, and the risk is scattered too. Grouping by feature flips this: the search code lives together, so the change, the review, and the test all focus on one place. That focus is the core benefit of the method, and everything else in this guide follows from it.
The Layers, From Top to Bottom
The method defines layers in a fixed order, and each layer has a clear job. At the top sits the app layer, which holds setup code that runs the whole project, such as routing, which means deciding which screen shows for each address, global providers, and global styles. Below it sits the pages layer, which holds full screens assembled from smaller blocks. A page should contain little logic of its own. It mainly arranges widgets and features into a screen that a user can visit, such as a product page or a settings page. Keeping pages thin matters, because a page that grows its own logic becomes a second home for feature code, and the structure starts to blur.
Next comes the widgets layer, which holds large, self-contained blocks that combine features and entities into a reusable section, such as a header, a product card list, or a checkout summary. A widget is bigger than a single button, and it often appears on several pages. Below widgets sits the features layer, which holds user actions, such as adding an item to a cart, liking a post, or filtering a list. A feature is the smallest piece of code that delivers value to a user on its own, and it usually combines interface, logic, and data calls for one action. This layer is where product work happens most often, so it deserves the most care.
Below features sits the entities layer, which holds business data and its basic operations, such as a product, an order, a user, or a ticket. An entity knows how to display itself and how to fetch its own data, but it does not know about user actions like checkout. That separation keeps entities stable while features change around them. At the bottom sits the shared layer, which holds code that many slices reuse, such as generic buttons, date helpers, and API clients. Shared code must stay generic: the moment it learns about a specific feature, it stops being shared and starts being a hidden dependency. The official documentation explains how each layer is meant to be used, including which imports each layer may make.
What a Slice Actually Contains
A slice is a folder inside a layer that holds everything for one part of the product. Take the search slice inside the features layer. It might hold a search box component, a hook that manages the search text, a function that calls the search API, which is the interface the frontend uses to ask the server for data, and a small test that checks the search behavior. All of those files live in one folder, so a developer who needs to change search behavior opens one place and sees the full picture. If the team later removes search from the product, it deletes one slice and checks the import rules to confirm nothing else depended on it. That clean deletion is one of the strongest practical benefits of the method.
Inside a slice, the method suggests segments, which are smaller folders for each kind of file, such as ui for interface pieces, model for logic and state, api for server calls, and lib for small helpers used only inside that slice. Segments keep a large slice tidy without creating a new top-level folder for every file type. A small slice may skip segments entirely and hold a few files directly, and that is fine. The rule is proportionate: use segments when a slice grows large enough that finding files inside it becomes slow, not from the first day. Teams that add segments too early pay the same folder tax they were trying to escape, just at a smaller scale.
Every slice also exposes a public API, which in this context means the small set of files other slices are allowed to import, usually through a single index file at the slice root. Everything else inside the slice stays private to that slice. This is the method's most important discipline, because it turns an informal hope into a checkable rule. If another slice imports a deep internal file, the import breaks the rule, and a linter, which is a tool that checks code for style and rule violations, can flag it. Without a public API, slices slowly leak into each other, and the structure becomes decoration. With it, slices stay honest, and the team can reason about one slice at a time. Our guide to connecting business systems cleanly applies the same idea at a larger scale: clear boundaries make large systems easier to change.
Import Rules That Keep Slices Honest
The import rules form a simple hierarchy. A layer may import from layers below it, but not from layers above it, and not from slices in the same layer except through narrow exceptions the project defines. In practice, a page may import widgets, features, entities, and shared code. A feature may import entities and shared code, but it should not import another feature directly, because features that depend on each other tend to merge into one tangled feature. An entity may import only shared code. Shared code may import nothing from the other layers, because it sits at the bottom and must stay neutral. These rules sound strict, but they encode common sense: low-level code should not depend on high-level code, or every change at the top ripples downward.
Two extra rules protect the structure. First, a slice may import another slice only through that slice's public API, never through a deep path into its internals. Second, slices in the same layer should avoid importing each other, because same-layer imports often signal that two slices are really one feature split in half, or that shared logic should move down to the shared layer. When a team needs cross-slice communication, the method points to patterns such as events, shared state at a lower layer, or a parent page that coordinates two features, rather than a direct import that couples them. Choosing the right pattern takes thought, and that thought is the point: the rule forces the team to notice coupling while it is still cheap to design around it.
Enforcement matters more than intention. Teams that write the rules in a document but never check them will drift within weeks, because deadlines reward the fastest import, not the cleanest one. The practical answer is tooling: configure a linter or a boundary checker to reject forbidden imports, and run that check in continuous integration, which means automated checks that run whenever code is proposed for merging. A rejected import then becomes a normal review conversation, not a personal argument. Teams should also review the shared layer regularly, because it attracts code that belongs in a feature. A shared folder that grows every week is a warning sign, not a success metric.
Layers Compared in One View
A table helps teams remember the layers during planning. If a piece of code fits two layers, ask which user job it serves. A product feature points to the features layer, a business object points to the entities layer, and code that everything uses, without knowing about any feature, points to the shared layer.
| Layer | Job in plain words | Example | May import from |
|---|---|---|---|
| App | Whole project setup | Routing and global styles | Pages and below |
| Pages | Full screens | Product page, settings page | Widgets and below |
| Widgets | Large reusable blocks | Header, checkout summary | Features and below |
| Features | Single user actions | Add to cart, filter list | Entities and shared |
| Entities | Business data objects | Product, order, user | Shared only |
| Shared | Generic reusable tools | Buttons, date helpers | Nothing above it |
Benefits Teams Actually Notice
The first benefit teams notice is faster onboarding, which means the time a new developer needs before making useful changes. A new hire can learn one slice, such as the cart feature, without learning the whole app first, because the slice holds its own interface, logic, and tests. This benefit grows with team size: on a team of twenty, a clear map can cut onboarding from a month to a week.
The second benefit is safer change. When a feature lives in one slice with a public API, a developer can change its internals with confidence that outside code only touches the public surface. Tests for the slice run in isolation, and reviewers can focus on the slice diff, which is the set of changes in one proposal, rather than scanning the whole project. Deletion becomes safer too, because removing a slice and checking its public API usage tells the team exactly what breaks. Teams that delete dead code regularly keep their projects smaller and faster, and this structure makes deletion a normal task instead of a scary project.
The third benefit is clearer ownership. A team can own a set of slices, such as all checkout slices, and other teams can use those slices through their public APIs without editing them. That boundary supports parallel work: two teams can build two features at once without stepping on each other's files. It also supports better reviews, because the owning team reviews changes to its slices, while other teams review only the public API impact. None of this happens automatically from folder names alone, but folders plus import rules plus ownership make it much more likely. Teams building larger products can pair this structure with custom software development work that plans slice ownership from the start, rather than adding it after the project grows tangled.
Limits and Honest Tradeoffs
The method is not free. It adds folders, index files, and import rules that a tiny project does not need. A landing page with three screens gains little from six layers, and the extra structure can slow a solo developer who already holds the whole app in mind. The method also adds learning time: developers used to grouping by file type need to learn the layers, the slice rules, and the public API habit, and the team needs to agree on where borderline code belongs. Without that agreement, two developers will place similar code in different layers, and the structure will look inconsistent even though everyone followed some rule.
Another limit is that the method organizes code, but it does not design the product. It cannot tell a team which features to build or how a screen should look. It also fits frontend code best: backend services often organize by domain or service boundary, and forcing frontend layers onto a backend can create awkward fits. Finally, overapplying the rules hurts too: slices should match real user jobs, not every button click, or the team ends up with dozens of slices that are harder to navigate than the original folders. For the wider product structure, teams turn to software architecture, which covers patterns beyond frontend layers.
Judging fit is therefore practical, not ideological. Choose this structure when the app has several real features, when more than a few developers work on it, when features change independently, and when deletion and reuse matter. Skip it, or adopt only the slice idea, when the app is small, short-lived, or built by one person for a single purpose. A team can also adopt it partially: group by feature inside the existing folders first, add public APIs second, and add layers third, as the app grows. Partial adoption done honestly beats full adoption done as decoration, because the benefits come from boundaries that are actually enforced, not from folder names that look right.
Adopting It Without a Rewrite
Rewriting a live app to fit a new structure is rarely worth the risk. A safer path starts with freezing the current structure for old code and applying the new rules to new code. Create the layers for new features only, and let old code stay where it is until it needs a real change. When an old feature needs work, move it into a slice as part of that change, and add its public API at the same time. This gradual path keeps the app shippable every week, because no single change tries to move everything at once. It also lets the team learn the rules on small, real work instead of on a large migration project.
Next, set up the checks before the rules matter. Add a boundary linter, write a one-page guide with examples from the team's own code, and review the first few slices together. Pay special attention to the shared layer, because early enthusiasm often dumps feature code there. A good test is to ask whether the code would still make sense if the feature that inspired it disappeared. If not, it belongs in that feature's slice.
Finally, measure whether the structure helps. Track onboarding time, the size of typical change proposals, and how often a change to one feature breaks another. If those numbers improve, the structure earns its overhead. If not, adjust the rules rather than defend them: slices may be too small, public APIs may be too wide, or the team may need better examples. For a related view on starting from one clear problem before building, see our guide on building applications around one clear problem.
Conclusion
Feature sliced design earns its place when a frontend app grows past the point where one person can hold it in mind. Group code by feature so changes stay local, group it into layers so responsibilities stay clear, expose a small public API per slice, and enforce the import rules with tooling. Adopt it gradually, keep the shared layer generic, and judge it by onboarding speed, change safety, and deletion confidence. Used that way, it turns a growing codebase from a maze into a map, and it lets teams add features without dreading the next change.
Frequently Asked Questions
What is feature sliced design in simple terms?
Feature sliced design is a way to organize frontend code so that code for one part of the app, such as checkout or search, lives together in a slice, and code is grouped into layers by its job, such as pages, widgets, features, entities, and shared tools. The goal is that a developer can find, change, and test one part without hunting through the whole project.
How is feature sliced design different from grouping code by file type?
Grouping by file type puts all components in one folder, all hooks in another, and all styles in a third, so one small change touches many folders. Grouping by feature puts the component, logic, and styles for one job together, so a change stays local, the code is easier to delete when the feature goes away, and new team members can learn one slice at a time.
When should a team avoid feature sliced design?
A very small app, such as a single landing page or a short prototype, often does not need this structure, because the extra folders add overhead without a payoff. Teams should also wait when the product is still changing fast and no stable features exist yet, and adopt the layers gradually as real features emerge.
Written by Danish Mustafa
Senior Agentic AI Engineer and Systems Architect
Danish Mustafa is a senior agentic AI engineer at Esaholic who builds AI agents, voice systems, and messaging systems for real customers.
Reviewed by Amir Iqbal
Senior AI Systems Architect and Lead Technical Reviewer
Amir Iqbal is a senior AI systems architect at Esaholic who reviews code and architecture and keeps live AI systems fast and reliable.
Relevant blogs
Rapid Prototyping: A Simple Guide to Fast Product Tests
How teams turn an idea into a testable model in days and learn fast without wasting budget.
React vs React Native: Which One Should You Pick
How React for the web differs from React Native for phones, and how to choose between them.
Cross Platform Mobile Programming Explained Simply
How one code base can serve Android and iPhone apps, where shared code helps, and where native code still wins.
Best AI Coding Assistants
How coding assistants help developers write, review, and fix code faster, and where human review stays essential.
Back to all blog guides · Back to Software Development guides · Explore Custom Software Development Services