#  Feature-Sliced Design in Practice

Most articles about Feature-Sliced Design show you the theory. The layer diagram. The import direction rules. The terminology (slices, segments, public API). Then they show you a contrived example with three components and call it a day.

That's not what this is.

I've applied FSD across three different products at genuinely different scales and domains: TrustRail (a B2B BNPL platform), Medcord (a hospital management system), and WordShot (a real-time multiplayer word game). All three follow the same structural rules. All three look different. The interesting part is understanding why — and where the pattern holds, where it bends, and when you shouldn't use it at all.

Let's get cracking,

* * *

## The Problem It Solves

Here's the folder structure I used to fall into on every project:

```plaintext
src/
├── components/     (47 files)
├── hooks/          (23 files)
├── utils/          (31 files)
├── types/          (18 files)
└── pages/          (12 files)
```

This feels tidy at the start. It's a disaster at scale.

Want to understand how multiplayer works in WordShot? You grep `/components` for multiplayer-related components, `/hooks` for multiplayer state, `/utils` for multiplayer helpers, and `/types` for the WebSocket event shapes. Those four folders tell you *what type of thing* each file is — they tell you nothing about *which feature* it belongs to.

Want to add a new feature? You touch files in four folders simultaneously, creating coupling between unrelated features at the filesystem level.

Want to remove the demo mode? You grep for `demo` across all four folders, hope you found everything, delete what you found, run the build, fix the broken imports you missed, repeat.

The folder-by-type structure is fine for small projects. The moment a project has more than about four independent features, it actively works against you.

* * *

## The Core Rule

Feature-Sliced Design's core rule can be stated in one sentence:

> **A feature owns everything it needs to function. Code that's needed by two or more features lives in** `shared/`**. Code that's needed by one feature lives inside that feature.**

Corollaries:

*   A feature never imports from another feature
    
*   A feature can import from `shared/`
    
*   `shared/` never imports from any feature
    
*   Code starts in the feature. It gets promoted to `shared/` when the second consumer arrives — not before
    

That's it. Everything else — the naming conventions, the sub-folder structure, the decision about whether to use sub-features — is derived from this rule applied to your specific domain.

* * *

## [TrustRail](https://github.com/spiderocious/trustrail-frontend): FSD at the Small End

TrustRail's [frontend](https://github.com/spiderocious/trustrail-frontend) is a React application with five features. It's the simplest case — a clean, unambiguous application of the pattern.

```plaintext
src/
├── features/
│   ├── auth/
│   │   ├── api/
│   │   │   ├── use-login.ts
│   │   │   └── use-register.ts
│   │   ├── guards/
│   │   │   ├── auth-guard.tsx
│   │   │   └── guest-guard.tsx
│   │   ├── helpers/
│   │   │   ├── nigerian-banks.ts
│   │   │   ├── token-storage.ts
│   │   │   └── validation.ts
│   │   ├── providers/
│   │   │   └── auth-provider.tsx
│   │   ├── screen/
│   │   │   ├── login-screen.tsx
│   │   │   ├── register-screen.tsx
│   │   │   └── parts/
│   │   │       ├── business-details-step.tsx
│   │   │       └── document-upload-step.tsx
│   │   └── auth.routes.tsx
│   │
│   ├── dashboard/          (overview metrics, application summary)
│   ├── applications/       (list, details, approve/decline)
│   ├── trust-wallets/      (BNPL product configuration)
│   └── public/             (the applicant-facing form)
│
└── shared/
    ├── constants/
    │   ├── api.ts
    │   └── routes/routes.ts
    ├── helpers/
    │   └── api-client.ts
    └── types/
        ├── auth.ts
        └── index.ts
```

Each feature contains its complete stack: API hooks (`use-login.ts`), screens (`login-screen.tsx`), component parts (`business-details-step.tsx`), helpers (`nigerian-banks.ts`), providers (`auth-provider.tsx`), and routes (`auth.routes.tsx`). Everything the `auth` feature needs to function is inside the `auth` folder.

`shared/` is minimal and intentional. It contains three things: API constants, the API client instance (used by every feature's API hooks), and base types. Nothing that belongs to a single feature.

Notice what's *not* in `shared/`: no UI components, no business logic, no screens, no feature-specific helpers. The Nigerian banks list (`nigerian-banks.ts`) is in `auth/helpers/` because only the registration form uses it. If another feature ever needed to display a bank selector, that file would move to `shared/helpers/`. It hasn't moved yet. It won't move until that second consumer exists.

### The Naming Convention

Every file follows a consistent naming pattern across the entire project:

*   API hooks: `use-{resource}.ts` — `use-login.ts`, `use-applications-list.ts`, `use-trust-wallet-details.ts`
    
*   Screens: `{resource}-screen.tsx` — `login-screen.tsx`, `application-details-screen.tsx`
    
*   Screen parts: `{part}-{type}.tsx` — `business-details-step.tsx`, `action-modal.tsx`
    
*   Providers: `{name}-provider.tsx` — `auth-provider.tsx`
    
*   Guards: `{condition}-guard.tsx` — `auth-guard.tsx`, `guest-guard.tsx`
    
*   Routes: `{feature}.routes.tsx` — `auth.routes.tsx`, `applications.routes.tsx`
    

This convention means a new developer navigating the codebase can predict file locations before finding them. "Where is the hook for fetching application details?" — `features/applications/api/use-application-details.ts`. No search required.

* * *

## [Medcord](http://github.com/spiderocious/medcord-app): FSD at the Large End

Medcord is a hospital management system. It handles patient registration, EMR records, lab orders, staff management, asset tracking, and workspace administration. It's substantially more complex than TrustRail — the kind of project where a poor folder structure becomes actively painful.

The top-level feature count is nine. But the more important distinction from TrustRail is that most features contain *sub-features* — a second level of the same pattern applied recursively.

```plaintext
src/
├── features/
│   ├── auth/
│   │   ├── api/
│   │   ├── features/
│   │   │   ├── forgot-password/
│   │   │   ├── login/
│   │   │   │   ├── parts/
│   │   │   │   │   ├── login-form.tsx
│   │   │   │   │   └── two-fa-step.tsx
│   │   │   │   └── screen/
│   │   │   │       └── login-screen.tsx
│   │   │   ├── register/
│   │   │   ├── reset-password/
│   │   │   └── setup-2fa/
│   │   └── shared/
│   │       └── parts/
│   │           └── auth-layout.tsx
│   │
│   ├── patients/
│   │   ├── features/
│   │   │   ├── patient-list/
│   │   │   ├── patient-profile/
│   │   │   ├── patient-register/
│   │   │   ├── patient-admitted/
│   │   │   ├── patient-checkedin/
│   │   │   └── patient-transfers/
│   │   └── shared/
│   │       └── types/
│   │           └── patient.ts
│   │
│   ├── emr/
│   │   ├── features/
│   │   │   ├── chart-overview/
│   │   │   ├── vitals/
│   │   │   ├── medications/
│   │   │   ├── procedures/
│   │   │   ├── history/
│   │   │   ├── immunizations/
│   │   │   ├── documents/
│   │   │   └── access-log/
│   │   └── shared/
│   │       ├── chart-layout.tsx
│   │       └── types/emr.ts
│   │
│   ├── labs/
│   ├── staff/
│   ├── assets/
│   ├── workspace/
│   ├── notifications/
│   └── queue/
│
└── shared/
    ├── api/
    ├── components/
    ├── guards/
    ├── hooks/
    ├── providers/
    ├── types/
    └── widgets/
        └── app-shell/
```

### When to Use Sub-Features

The decision to make `patients` a domain with sub-features rather than a single flat feature comes down to one question: *do these sub-capabilities have meaningfully independent lifecycles?*

A patient list screen and a patient profile screen do not share state. A patient registration flow and a patient transfer flow have entirely different API hooks, different screen components, and different form logic. They happen to be about the same domain entity (the patient), but they're developed, tested, and modified independently.

If they lived in a flat `patients/` folder, you'd have a folder with 30+ files that are only loosely related. Sub-features give each capability its own clean boundary. `patient-register/` contains exactly what you need to understand the registration flow. `patient-transfers/` contains exactly what you need to understand the transfer flow. Neither reaches into the other.

The rule I follow: introduce a sub-feature level when a top-level feature would otherwise contain more than about 15–20 files, or when distinct capabilities within that feature need to be understood independently.

### Feature-Level `shared/`

Each top-level domain has its own `shared/` folder for code shared across its sub-features but not needed by other top-level features:

*   `emr/shared/chart-layout.tsx` — the layout wrapper used by every EMR sub-screen (vitals, medications, procedures all share the same chrome)
    
*   `emr/shared/types/emr.ts` — the TypeScript types for EMR entities
    
*   `patients/shared/types/patient.ts` — the patient type shared across all patient sub-features
    

This is the same promotion rule applied one level down: code starts in the sub-feature, moves to the domain-level `shared/` when the second sub-feature needs it, moves to the top-level `shared/` when two top-level features need it.

### The App-Level `shared/`

Medcord's top-level `shared/` is more substantial than TrustRail's because the application is larger:

```plaintext
shared/
├── api/
│   └── use-hospital-by-slug.ts   (used by auth + workspace + all features)
├── components/
│   └── entity-link.tsx           (universal cross-entity navigation)
├── guards/
│   ├── auth-guard.tsx
│   └── hospital-guard.tsx
├── hooks/
│   ├── use-auth.ts
│   ├── use-hospital-slug.ts
│   └── use-permissions.ts
├── providers/
│   ├── auth-provider.tsx
│   └── user-bootstrap.tsx
├── types/                        (shared domain types: Hospital, Patient, Staff...)
└── widgets/
    └── app-shell/                (sidebar, topbar, user menu — used by every feature)
```

`use-permissions.ts` is in `shared/hooks/` because RBAC permission checking is used across 6 different features (staff management, patient registration, EMR write access, lab order creation, asset management, workspace settings). Moving it to `shared/` the first time two features needed it was the right call — it has never needed to move again.

* * *

## WordShot: The Decision That Proved the Pattern

WordShot's FSD structure was discussed in the previous series of posts on this blog, but it's worth revisiting through the lens of a specific decision: demo mode.

Demo mode is an interactive walkthrough for first-time users. It shows the game flow step-by-step without making any API calls, without joining a real game room, without touching the WebSocket. It's entirely self-contained.

Before FSD, building demo mode in the old folder-by-type structure would have required:

*   Adding demo-specific state to the existing Redux store (or creating a parallel store)
    
*   Adding conditional rendering to game components to handle "am I in demo mode?" checks
    
*   Threading a `isDemoMode` prop through the component tree
    
*   Hoping that game logic changes don't break the demo flow
    

With FSD, demo mode was `features/demo/`. It contained its own state machine, its own screen components, its own mock data, its own routing. It shared only the base UI components from `shared/ui/` that it visually needed. The `game` and `multiplayer` features were not touched.

The demo took two days to build because the structure made it easy. There was no untangling to do. No "wait, this component is used by both the real game and the demo, how do I make them diverge?" There was no entanglement in the first place.

When the decision to remove demo mode later came up, it was a folder deletion and three route changes. That's the proof.

* * *

## The `shared/` Discipline

The most common way FSD falls apart in practice is `shared/` becoming a dumping ground.

I've seen codebases that claim to use FSD where `shared/` contains:

*   Feature-specific components that "might be reused someday"
    
*   Utility functions used by exactly one feature
    
*   A `misc/` folder
    
*   Business logic that belonged in a feature service
    

When `shared/` is polluted, the cross-feature import rule becomes unenforceable because everything is already in `shared/`. Features stop having clear boundaries. The original problem returns in a different location.

The discipline is: `shared/` **must earn every file it contains.** A file enters `shared/` when a second consumer appears. Not when you think it might be reused. Not because it "looks generic." When the second consumer actually arrives and needs it.

In practice, I've found that healthy `shared/` layers contain:

*   The API client instance (one, used everywhere)
    
*   Auth primitives (the session hook, the auth provider, route guards)
    
*   The app shell / layout chrome
    
*   Base TypeScript types for domain entities that genuinely cross feature boundaries
    
*   A small set of UI utilities (token-safe wrappers, error display primitives)
    

Healthy `shared/` layers do not contain:

*   Form components (forms are feature-specific in almost every real codebase)
    
*   Anything with "business logic" in the filename or in the code
    
*   More than about 30–40 files total in a mid-sized application
    

* * *

## Where FSD Bends

Three real failure modes I've encountered:

**The cross-feature import temptation.** You're building `applications` in TrustRail and you need the list of trust wallets to render a dropdown. `trust-wallets` has a `use-trust-wallets.ts` hook that fetches exactly the data you need. The temptation is to import it directly. Don't.

The correct move: expose the data through a shared type and a prop or context, not through a direct import. If `applications` needs to know which trust wallet an application belongs to, that relationship lives in the application data itself, not in a cross-feature import.

If two features genuinely need to share data access at a deep level, that's a signal that the feature boundary was drawn incorrectly. Merge them, or extract the shared data concern to `shared/`.

**The premature sub-feature.** Adding sub-feature levels when a flat feature would work fine adds cognitive overhead without benefit. `auth/features/login/screen/login-screen.tsx` has one more path component than `auth/screen/login-screen.tsx`. If the auth feature has three flows (login, register, forgot password) and each is less than 10 files, a flat structure is cleaner.

The sub-feature level is worth introducing when the sub-capability count makes the flat structure unwieldy — Medcord's `patients/` with six sub-features is clearer with the sub-level than without it.

**The naming inconsistency.** FSD's value compounds when naming is consistent. When half the features use `use-{resource}.ts` for hooks and the other half use `{resource}Hook.ts`, the predictability breaks. When some screens are `{feature}-screen.tsx` and others are `{Feature}Page.tsx`, grep becomes your primary navigation tool again.

Establish the convention once, at project start, in a brief README. Enforce it in code review. It takes a day to establish and saves weeks over the lifetime of the project.

* * *

## When Not to Use FSD

Feature-Sliced Design has overhead. For the right project, the overhead pays for itself quickly. For the wrong project, it's bureaucratic noise.

**Don't use FSD when:** the project has fewer than 3–4 independent features. A landing page site with one interactive form. A prototype with one screen. A personal tool with one workflow. Flat folder structures are faster to navigate and faster to change for small projects.

**Do use FSD when:** the project is expected to grow, different parts of it are owned or will be owned by different people, features need to be added and removed without cross-feature contamination, or you've already experienced the folder-by-type pain and want to not experience it again.

The most predictable sign that a project needs FSD: you've opened the wrong file three times in the last day because two features have a component with the same name in the same `components/` folder.

* * *

## Evaluation

Across TrustRail, Medcord, and WordShot, FSD produced several consistent properties:

**Feature isolation** — Changes to one feature never broke another. In Medcord, shipping a major rework of the patient transfer flow required touching exactly five files, all inside `patients/features/patient-transfers/`. No other feature was touched.

**Predictable navigation** — A developer new to TrustRail could find any file in the codebase by following the naming convention without ever searching. Feature → sub-folder → file. The structure teaches itself.

**Clean deletions** — WordShot's demo mode removal was a folder deletion. In Medcord, the `queue/` feature (a work-in-progress) could be removed at any point without touching anything else. Feature-scoped code deletes cleanly.

**Proportional complexity** — TrustRail's five-feature flat structure and Medcord's nine-domain two-level structure are both instances of the same pattern, at different scales. The pattern doesn't impose Medcord's complexity on TrustRail or TrustRail's simplicity on Medcord.

* * *

## The Honest Conclusion

Feature-Sliced Design is not a silver bullet. It won't fix bad abstractions, poor naming, or unclear feature boundaries. What it does is give you a consistent, enforceable rule for where code lives — one that scales with project growth instead of collapsing under it.

The folder-by-type structure tells you what your code is. FSD tells you what it does and who owns it. At small scale, the distinction doesn't matter much. At medium and large scale, it's the difference between a codebase you can navigate and one you can only survive.

**The structure is not the architecture. But the right structure makes the architecture legible.**
