In modern React application development, maintaining scalability and organization as your codebase grows is crucial. Combining Feature-Sliced Design (FSD) with Domain-Driven Design (DDD) principles offers a powerful approach to structuring your application for long-term maintainability. Let’s explore how we’ve implemented this in our project structure.
Understanding the Architectural Approach
Feature-Sliced Design (FSD)
FSD organizes code by features rather than technical layers, keeping all related code for a feature (components, styles, logic) together. This makes features more modular and easier to reason about.
Domain-Driven Design (DDD)
DDD focuses on modeling your application around the business domain, using a common language shared between developers and domain experts.
Our Project Structure Breakdown
src/
├── assets/ # Static assets
├── shared/ # Reusable components/utils across features
├── core/ # Application core infrastructure
│ ├── api/ # API configurations
│ ├── router/ # Routing logic
│ ├── i18n/ # Internationalization setup
├── features/ # Feature modules
│ └── auth/ # Authentication feature
│ ├── components/ # Feature-specific components
│ ├── constants/ # Feature constants
│ ├── models/ # Domain models
│ ├── pages/ # Page compositions
│ ├── services/ # Business logic
│ ├── stores/ # State management
│ ├── queries/ # Data fetching logic
│ └── routes/ # Feature routing
├── lib/ # External libraries/UI kits
├── styles/ # Global styles
└── main entry files
Key Implementation Details
1. Feature Module: Authentication
Our auth
feature demonstrates how we combine FSD and DDD:
features/
auth/
components/LoginForm/ # Presentational components
models/ # Domain models and types
services/ # Business logic services
stores/ # State management
queries/ # Data fetching with React Query
pages/ # Page composition
routes/ # Feature routes
This structure keeps all authentication-related code together while separating concerns within the feature.
2. Core Application Infrastructure
The core
directory contains cross-cutting concerns:
core/
api/ # Axios instance and interceptors
router/ # Main router configuration
i18n/ # Translation setup
components/ # Truly global UI components
3. Shared vs Core Components
- shared/components: For components reused across features
- core/components: For truly global components (like layout frames)
- feature/components: For feature-specific components
Benefits of This Structure
- Improved Maintainability: Features can be developed, tested, and maintained independently
- Clear Separation of Concerns: UI, business logic, and state management are clearly separated
- Better Scalability: New features can be added without restructuring existing code
- Enhanced Team Collaboration: Features can be owned by different teams
- Domain Alignment: Code organization reflects business domains
Implementation Example: Authentication Flow
Here’s how the pieces work together in our auth feature:
// features/auth/models/auth.model.ts
export interface User {
id: string;
email: string;
// ... other user properties
}
// features/auth/services/login.service.ts
export class AuthService {
async login(email: string, password: string): Promise<User> {
// Business logic implementation
}
}
// features/auth/stores/auth.store.ts
const useAuthStore = create(() => ({
user: null as User | null,
isAuthenticated: false,
// ... actions
}));
// features/auth/pages/login.page.tsx
export function LoginPage() {
const { login } = useAuthStore();
return (
<LoginForm onSubmit={login} />
);
}
Best Practices We Follow
- Feature Isolation: Features should minimize dependencies on other features
- Dependency Direction: Higher-level features can depend on lower-level ones, but not vice versa
- API Contracts: Features communicate through well-defined APIs (services, hooks)
- Domain Modeling: Invest time in proper domain modeling before implementation
- Testing Strategy: Each feature should have its own test suite
Challenges and Solutions
- Circular Dependencies: Avoided by clear layer separation and dependency rules
- Shared State: Managed through clearly defined stores and services
- Feature Communication: Done via events or through parent components
- Initial Setup Complexity: Offset by long-term maintainability benefits
End Card
Combining Feature-Sliced Design with Domain-Driven principles in React applications creates a maintainable, scalable architecture that grows gracefully with your application. Our structure provides clear separation of concerns while keeping related code cohesive.
As your application evolves, this approach makes it easier to:
- Onboard new developers
- Test features in isolation
- Refactor or replace features
- Maintain a clear understanding of your domain
Leave a Reply