Building React Apps with Feature-Sliced Design and Domain-Driven Principles

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

  1. Improved Maintainability: Features can be developed, tested, and maintained independently
  2. Clear Separation of Concerns: UI, business logic, and state management are clearly separated
  3. Better Scalability: New features can be added without restructuring existing code
  4. Enhanced Team Collaboration: Features can be owned by different teams
  5. 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

  1. Feature Isolation: Features should minimize dependencies on other features
  2. Dependency Direction: Higher-level features can depend on lower-level ones, but not vice versa
  3. API Contracts: Features communicate through well-defined APIs (services, hooks)
  4. Domain Modeling: Invest time in proper domain modeling before implementation
  5. Testing Strategy: Each feature should have its own test suite

Challenges and Solutions

  1. Circular Dependencies: Avoided by clear layer separation and dependency rules
  2. Shared State: Managed through clearly defined stores and services
  3. Feature Communication: Done via events or through parent components
  4. 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

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *