Feature-Based & Clean Architecture
📖 Concept
Feature-based architecture organizes code by business domain rather than technical concern. Instead of grouping all components together, all hooks together, all services together — you group everything related to a feature in one directory.
Why this matters at scale:
- 15 engineers working on the same
/componentsfolder = merge conflicts, unclear ownership - Feature folders = clear ownership boundaries, independent deployment, easier testing
- New engineers find related code in ONE place instead of hunting across 10 directories
Traditional (Technical grouping) — breaks at scale:
src/
components/ ← 200+ files, no clear ownership
hooks/ ← 80+ hooks from every feature mixed
services/ ← network, auth, storage, analytics all mixed
screens/ ← flat list of 50+ screens
redux/
actions/ ← all actions mixed
reducers/ ← all reducers mixed
Feature-based — scales with team:
src/
features/
auth/
screens/
components/
hooks/
api/
store/
__tests__/
index.ts ← Public API (what other features can import)
checkout/
screens/
components/
hooks/
api/
store/
__tests__/
index.ts
feed/
...
shared/ ← Cross-feature utilities
components/ ← Design system components
hooks/ ← Generic reusable hooks
utils/ ← Formatters, validators
api/ ← Base API client
platform/ ← App-level infrastructure
navigation/
analytics/
errorHandling/
storage/
Clean Architecture principles applied to React Native:
┌─────────────────────────────────┐
│ UI Layer (Screens) │ ← Knows about: Presentation, Domain
│ React Components, Navigation │
├─────────────────────────────────┤
│ Presentation Layer │ ← Knows about: Domain only
│ ViewModels, Presenters, Hooks │
├─────────────────────────────────┤
│ Domain Layer │ ← Knows about: NOTHING (pure business logic)
│ Entities, Use Cases, Interfaces │
├─────────────────────────────────┤
│ Data Layer │ ← Implements: Domain interfaces
│ API clients, Storage, Cache │
└─────────────────────────────────┘
The dependency rule: Dependencies point INWARD. The Domain layer has ZERO dependencies on React, React Native, or any framework. This makes business logic testable without mocking the entire framework.
💻 Code Example
1// === FEATURE-BASED ARCHITECTURE IN PRACTICE ===23// features/checkout/index.ts — Public API4// Only export what other features need5export { CheckoutScreen } from './screens/CheckoutScreen';6export { useCartTotal } from './hooks/useCartTotal';7export type { CartItem, CheckoutResult } from './types';8// Everything else is PRIVATE to this feature910// features/checkout/domain/entities.ts — Pure domain11interface CartItem {12 id: string;13 productId: string;14 name: string;15 price: number;16 quantity: number;17}1819interface CheckoutResult {20 orderId: string;21 status: 'success' | 'failed' | 'pending';22 total: number;23}2425// features/checkout/domain/useCases.ts — Business logic (NO React)26interface CartRepository {27 getItems(): Promise<CartItem[]>;28 addItem(item: CartItem): Promise<void>;29 removeItem(id: string): Promise<void>;30}3132interface PaymentService {33 processPayment(amount: number, method: PaymentMethod): Promise<PaymentResult>;34}3536class CheckoutUseCase {37 constructor(38 private cartRepo: CartRepository,39 private paymentService: PaymentService,40 private analyticsService: AnalyticsService,41 ) {}4243 async execute(paymentMethod: PaymentMethod): Promise<CheckoutResult> {44 const items = await this.cartRepo.getItems();4546 if (items.length === 0) {47 throw new CheckoutError('Cart is empty', 'EMPTY_CART');48 }4950 const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);5152 const paymentResult = await this.paymentService.processPayment(total, paymentMethod);5354 if (paymentResult.status === 'success') {55 await this.cartRepo.clear();56 this.analyticsService.trackPurchase(total, items.length);57 }5859 return {60 orderId: paymentResult.orderId,61 status: paymentResult.status,62 total,63 };64 }65}6667// features/checkout/data/CartRepositoryImpl.ts — Data layer68class CartRepositoryImpl implements CartRepository {69 constructor(70 private api: APIClient,71 private localStorage: StorageService,72 ) {}7374 async getItems(): Promise<CartItem[]> {75 try {76 // Try API first77 const items = await this.api.get('/cart');78 await this.localStorage.set('cart', items); // Cache locally79 return items;80 } catch (error) {81 // Fallback to local cache when offline82 return this.localStorage.get('cart') ?? [];83 }84 }85}8687// features/checkout/hooks/useCheckout.ts — Presentation layer88function useCheckout() {89 const [state, setState] = useState<CheckoutState>({ status: 'idle' });9091 // Dependencies injected (not imported directly)92 const checkoutUseCase = useInjection(CheckoutUseCase);9394 const execute = useCallback(async (paymentMethod: PaymentMethod) => {95 setState({ status: 'processing' });96 try {97 const result = await checkoutUseCase.execute(paymentMethod);98 setState({ status: 'success', result });99 } catch (error) {100 setState({ status: 'error', error: error as CheckoutError });101 }102 }, [checkoutUseCase]);103104 return { state, execute };105}106107// features/checkout/screens/CheckoutScreen.tsx — UI layer108function CheckoutScreen() {109 const { state, execute } = useCheckout();110 // UI only — delegates all logic to the hook/use case111 // ...112}
🏋️ Practice Exercise
Architecture Exercises:
- Refactor a flat-structured React Native project into feature-based architecture — document the before/after
- Create a feature module with public API (index.ts) and verify that other features can't import private internals (use ESLint import restrictions)
- Implement a use case class with dependency injection that has ZERO React imports — test it with plain Jest
- Draw the dependency diagram for a 5-feature app showing which features depend on what
- Set up a monorepo with shared packages (ui-kit, api-client, analytics) and feature apps
- Write architectural documentation for your app that a new engineer can understand in 30 minutes
⚠️ Common Mistakes
Over-engineering small apps with clean architecture — for apps with <10 screens, feature folders + hooks is sufficient
Leaking implementation details through feature exports — index.ts should only export what other features genuinely need
Creating circular dependencies between features — Feature A imports from Feature B which imports from Feature A
Not enforcing architectural boundaries — without lint rules or CI checks, developers drift back to dumping code in shared folders
Making the domain layer depend on React or RN — defeats the purpose of clean architecture; domain should be pure TypeScript
💼 Interview Questions
🎤 Mock Interview
Mock interview is powered by AI for Feature-Based & Clean Architecture. Login to unlock this feature.