

Feature-Sliced Design in Frontend

- Feature-Sliced Design vs. Clean Architecture
- Project Structure (FSD)
- Feature-Sliced Design (FSD)
- 2. Clean Architecture
- Project Structure (Clean Architecture)
- Layering in Clean Architecture
- Advantages of Clean Architecture
- Implementing Search Feature (Clean Architecture)
- Use Case: domain/useCases/SearchUseCase.ts
- Infrastructure Layer: API Implementation (infrastructure/repositories/SearchApi.ts)
- Application Layer: Repository Interface (domain/repositories/ISearchRepository.ts)
- Presentation Layer: UI (presentation/components/Search.tsx)
- Comparison of Code Structure
- Which One Should You Use?
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
Feature-Based Structure – The application is divided into slices (features), not by technical layers. Each feature is self-contained.
Encapsulation – Each feature manages its own UI components, state, logic, and API interactions.
Explicit Dependencies – Prevents deep interdependencies between modules by ensuring clear, high-level relationships.
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
Aspect | Feature-Sliced Design | Clean Architecture |
Organization | Feature-driven folders (features/search/ ) | Layered folders (domain/ , application/ , presentation/ ) |
Encapsulation | UI, state, and API calls in a feature | Strict separation between UI and business logic |
State Handling | Encapsulated in useSearch hook | State is only in UI; logic is in use cases |
API Interaction | Called directly from feature | Managed through repositories |
Scalability | Scales easily by adding new feature folders | Scales well but requires discipline |
Best Use Case | UI-heavy applications, product-driven development | Applications 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


Understanding and Solving Performance Issues in Software Applications
