Feature-Sliced Design in Frontend

Feature-Sliced Design in Frontend

Feature-Sliced Design (FSD) is an architectural methodology for scaffolding front-end applications. Simply put, it's a compilation of rules and conventions on organizing code. The main purpose of this methodology is to make the project more understandable and stable in the face of ever-changing business requirements.
Feature-Sliced Design in Frontend

Feature-Sliced Design vs. Clean Architecture

Feature-Sliced Design (FSD) is an architectural methodology for scaffolding front-end applications. Simply put, it's a compilation of rules and conventions on organizing code. The main purpose of this methodology is to make the project more understandable and stable in the face of ever-changing business requirements.

1. Feature-Sliced Design

Project Structure (FSD)

src/
 ├── app/              # Global app setup (store, routing, providers)
 ├── processes/        # Cross-feature flows (auth, checkout)
 ├── pages/            # Pages combining features
 ├── widgets/          # UI components (Navbar, Sidebar)
 ├── features/         # Business logic-driven UI components
 │    ├── search/      # Search feature (UI, state, API)
 │    ├── cart/        # Cart feature (UI, state, API)
 ├── entities/         # Core domain models (User, Product)
 ├── shared/           # Utilities, UI components, hooks

Feature-Sliced Design (FSD)

Feature-Sliced Design is a frontend-oriented architecture that emphasizes organizing code based on features rather than technical layers. It is designed to improve scalability, separation of concerns, and maintainability in complex UI applications.

Core Principles of FSD

  1. Feature-Based Structure – The application is divided into slices (features), not by technical layers. Each feature is self-contained.

  2. Encapsulation – Each feature manages its own UI components, state, logic, and API interactions.

  3. Explicit Dependencies – Prevents deep interdependencies between modules by ensuring clear, high-level relationships.

  4. Scalability – New features can be added without affecting existing ones.

Layering in FSD

FSD consists of several logical layers:

  • App Layer – Global configurations (providers, routing, store setup).

  • Processes Layer – Cross-feature flows (authentication, checkout).

  • Pages Layer – Pages with multiple feature compositions.

  • Widgets Layer – Complex reusable UI parts (e.g., Navbar, Sidebar).

  • Features Layer – Business logic-driven UI components (e.g., Search, Cart).

  • Entities Layer – Core domain models, like a User or Product.

  • Shared Layer – Generic utilities, UI components, and API helpers.

Advantages of FSD

✔ Scales Well – Feature-based division makes adding new features seamless.
✔ Clear Boundaries – Business logic and UI separation improve modularity.
✔ Encapsulation – Reduces unintended coupling and dependency issues.
✔ Improved Collaboration – Teams can work independently on different features.

Feature Example: Search (FSD Approach)

Each feature encapsulates UI, state, and API interactions.

Search Feature: features/search/index.ts

import { SearchBar } from "./ui/SearchBar";
import { useSearch } from "./model/useSearch";
export const Search = () => {
 const { results, query, setQuery } = useSearch();
 return <SearchBar query={query} setQuery={setQuery} results={results} />;
};

UI Component: features/search/ui/SearchBar.tsx

import React from "react";
interface SearchBarProps {
 query: string;
 setQuery: (q: string) => void;
 results: string[];
}
export const SearchBar: React.FC<SearchBarProps> = ({ query, setQuery, results }) => {
 return (
   <div>
     <input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search..." />
     <ul>
       {results.map((item, index) => (
         <li key={index}>{item}</li>
       ))}
     </ul>
   </div>
 );
};

State Management: features/search/model/useSearch.ts

import { useState, useEffect } from "react";
import { fetchSearchResults } from "../api/searchApi";
export const useSearch = () => {
 const [query, setQuery] = useState("");
 const [results, setResults] = useState<string[]>([]);
 useEffect(() => {
   if (query.length > 2) {
     fetchSearchResults(query).then(setResults);
   }
 }, [query]);
 return { query, setQuery, results };
};

API Call: features/search/api/searchApi.ts

export const fetchSearchResults = async (query: string): Promise<string[]> => {
 const response = await fetch(`/api/search?q=${query}`);
 return response.json();
};

2. Clean Architecture

Project Structure (Clean Architecture)

src/
├── domain/          # Business rules (Entities, UseCases)
├── application/     # Application logic (Services, Interfaces)
├── infrastructure/  # API, LocalStorage, Adapters
├── presentation/    # React Components (UI)
├── main.tsx        # Application entry point

Clean Architecture, initially introduced by Robert C. Martin, is commonly used in backend applications but can be adapted for frontend projects. It separates concerns based on technical layers, ensuring a clear dependency rule where inner layers remain independent of outer ones.

Layering in Clean Architecture

A typical Clean Architecture frontend includes:

  • Entities (Domain Layer) – Business rules and core models.

  • Use Cases (Application Layer) – Business logic in an abstract way.

  • Adapters (Infrastructure Layer) – APIs, data sources, local storage.

  • UI (Presentation Layer) – React/Vue/Svelte components, state management.

Advantages of Clean Architecture

Strict Separation of Concerns – Keeps business logic independent of UI frameworks.

Highly Maintainable – Code is easier to refactor and test.

Reusability – The business logic can be reused across multiple UI implementations.

Implementing Search Feature (Clean Architecture)

Here, the UI layer doesn’t interact directly with data fetching. Instead, it calls use cases, which interact with repositories.

Domain Layer: domain/entities/SearchResult.ts

export class SearchResult {
 constructor(public title: string, public url: string) {}
}

Use Case: domain/useCases/SearchUseCase.ts

import { ISearchRepository } from "../repositories/ISearchRepository";
import { SearchResult } from "../entities/SearchResult";
export class SearchUseCase {
 constructor(private searchRepo: ISearchRepository) {}
 async execute(query: string): Promise<SearchResult[]> {
   if (query.length < 3) return [];
   return await this.searchRepo.search(query);
 }
}

Infrastructure Layer: API Implementation (infrastructure/repositories/SearchApi.ts)

import { ISearchRepository } from "../../domain/repositories/ISearchRepository";
import { SearchResult } from "../../domain/entities/SearchResult";
export class SearchApi implements ISearchRepository {
 async search(query: string): Promise<SearchResult[]> {
   const response = await fetch(`/api/search?q=${query}`);
   const results = await response.json();
   return results.map((r: any) => new SearchResult(r.title, r.url));
 }
}

Application Layer: Repository Interface (domain/repositories/ISearchRepository.ts)

import { SearchResult } from "../entities/SearchResult";
export interface ISearchRepository {
 search(query: string): Promise<SearchResult[]>;
}

Presentation Layer: UI (presentation/components/Search.tsx)

import { useState } from "react";
import { SearchUseCase } from "../../domain/useCases/SearchUseCase";
import { SearchApi } from "../../infrastructure/repositories/SearchApi";
const searchUseCase = new SearchUseCase(new SearchApi());
export const Search = () => {
 const [query, setQuery] = useState("");
 const [results, setResults] = useState<{ title: string; url: string }[]>([]);
 const handleSearch = async () => {
   const results = await searchUseCase.execute(query);
   setResults(results);
 };
 return (
   <div>
     <input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search..." />
     <button onClick={handleSearch}>Search</button>
     <ul>
       {results.map((item, index) => (
         <li key={index}>
           <a href={item.url}>{item.title}</a>
         </li>
       ))}
     </ul>
   </div>
 );
};

Comparison of Code Structure

AspectFeature-Sliced DesignClean Architecture
OrganizationFeature-driven folders (features/search/)Layered folders (domain/, application/, presentation/)
EncapsulationUI, state, and API calls in a featureStrict separation between UI and business logic
State HandlingEncapsulated in useSearch hookState is only in UI; logic is in use cases
API InteractionCalled directly from featureManaged through repositories
ScalabilityScales easily by adding new feature foldersScales well but requires discipline
Best Use CaseUI-heavy applications, product-driven developmentApplications with complex business logic

Which One Should You Use?

Choose FSD if your project is frontend-heavy and needs fast, modular development.

Choose Clean Architecture if your project has complex business rules and needs domain separation.

Hybrid Approach: Use FSD for UI structuring and Clean Architecture inside features where domain logic is critical.

My current project is using Hybrid Approach.


For more information, let's Like & Follow MFV sites for updatingblog, best practices, career stories of Forwardians at:

Facebook: https://www.facebook.com/moneyforward.vn 

Linkedin: https://www.linkedin.com/company/money-forward-vietnam/ 

Youtube: https://www.youtube.com/channel/UCtIsKEVyMceskd0YjCcfvPg 

 

More like this

How to create your own gem in Ruby
May 24, 2024

How to create your own gem in Ruby

Design pattern in SCI
Feb 02, 2024

Design pattern in SCI