Concept
File-type grouping vs Feature-based grouping
In early development stages, most codebases start with file-type grouping (screaming architecture anomaly):
src/
├── components/
│ ├── UserProfile.tsx
│ └── UserSettings.tsx
├── hooks/
│ ├── useUser.ts
│ └── useSettings.ts
└── services/
├── userService.ts
└── settingsService.tsAs the app grows, adding or modifying a single feature requires file-hopping across multiple folders. This increases cognitive load and results in loose coupling of features.
Feature-based grouping consolidates related files into one folder representing a business domain or feature:
src/
├── features/
│ ├── auth/
│ └── billing/
│ ├── components/
│ │ ├── PricingTable.tsx
│ │ └── CheckoutButton.tsx
│ ├── hooks/
│ │ └── useSubscription.ts
│ ├── api/
│ │ └── billingClient.ts
│ └── index.ts <-- PUBLIC APIPublic API boundaries via index.ts
To keep features decoupled, they must communicate only through a strict public interface. The index.ts file at the feature root acts as a barrel file containing explicit exports.
Other features must never import internal files directly:
// ❌ WRONG: importing internals breaks encapsulation
import { CheckoutButton } from '../billing/components/CheckoutButton';
// CORRECT: importing through feature public API
import { CheckoutButton } from '../billing';Use ESLint rules (no-restricted-imports) to block direct imports of internal directories.
Common Mistakes
1. The "shared" folder dumping ground
When components are reused, developers throw them into a global shared/ or common/ folder. This folder becomes a massive junk drawer. Keep components inside feature folders until they are genuinely needed by multiple unrelated domains.
2. Deeply nested feature subdirectories
Avoid nesting feature folders more than 2-3 levels deep. Deep nesting makes file paths hard to read and import.
Best Practices
- Self-contained features: A feature folder should contain its own assets, tests, styles, state management, and routes.
- Easy deletion: If a feature is deprecated, you should be able to delete the entire feature folder without breaking the rest of the application.
- Enforce boundaries: Use tooling (like Dependency Cruiser or Nx boundaries) to enforce that features cannot import from each other's internals.
