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 /components folder = 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

codeTap to expand ⛶
1// === FEATURE-BASED ARCHITECTURE IN PRACTICE ===
2
3// features/checkout/index.ts — Public API
4// Only export what other features need
5export { CheckoutScreen } from './screens/CheckoutScreen';
6export { useCartTotal } from './hooks/useCartTotal';
7export type { CartItem, CheckoutResult } from './types';
8// Everything else is PRIVATE to this feature
9
10// features/checkout/domain/entities.ts — Pure domain
11interface CartItem {
12 id: string;
13 productId: string;
14 name: string;
15 price: number;
16 quantity: number;
17}
18
19interface CheckoutResult {
20 orderId: string;
21 status: 'success' | 'failed' | 'pending';
22 total: number;
23}
24
25// 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}
31
32interface PaymentService {
33 processPayment(amount: number, method: PaymentMethod): Promise<PaymentResult>;
34}
35
36class CheckoutUseCase {
37 constructor(
38 private cartRepo: CartRepository,
39 private paymentService: PaymentService,
40 private analyticsService: AnalyticsService,
41 ) {}
42
43 async execute(paymentMethod: PaymentMethod): Promise<CheckoutResult> {
44 const items = await this.cartRepo.getItems();
45
46 if (items.length === 0) {
47 throw new CheckoutError('Cart is empty', 'EMPTY_CART');
48 }
49
50 const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
51
52 const paymentResult = await this.paymentService.processPayment(total, paymentMethod);
53
54 if (paymentResult.status === 'success') {
55 await this.cartRepo.clear();
56 this.analyticsService.trackPurchase(total, items.length);
57 }
58
59 return {
60 orderId: paymentResult.orderId,
61 status: paymentResult.status,
62 total,
63 };
64 }
65}
66
67// features/checkout/data/CartRepositoryImpl.ts — Data layer
68class CartRepositoryImpl implements CartRepository {
69 constructor(
70 private api: APIClient,
71 private localStorage: StorageService,
72 ) {}
73
74 async getItems(): Promise<CartItem[]> {
75 try {
76 // Try API first
77 const items = await this.api.get('/cart');
78 await this.localStorage.set('cart', items); // Cache locally
79 return items;
80 } catch (error) {
81 // Fallback to local cache when offline
82 return this.localStorage.get('cart') ?? [];
83 }
84 }
85}
86
87// features/checkout/hooks/useCheckout.ts — Presentation layer
88function useCheckout() {
89 const [state, setState] = useState<CheckoutState>({ status: 'idle' });
90
91 // Dependencies injected (not imported directly)
92 const checkoutUseCase = useInjection(CheckoutUseCase);
93
94 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]);
103
104 return { state, execute };
105}
106
107// features/checkout/screens/CheckoutScreen.tsx — UI layer
108function CheckoutScreen() {
109 const { state, execute } = useCheckout();
110 // UI only — delegates all logic to the hook/use case
111 // ...
112}

🏋️ Practice Exercise

Architecture Exercises:

  1. Refactor a flat-structured React Native project into feature-based architecture — document the before/after
  2. Create a feature module with public API (index.ts) and verify that other features can't import private internals (use ESLint import restrictions)
  3. Implement a use case class with dependency injection that has ZERO React imports — test it with plain Jest
  4. Draw the dependency diagram for a 5-feature app showing which features depend on what
  5. Set up a monorepo with shared packages (ui-kit, api-client, analytics) and feature apps
  6. 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.