# ADR-005: Feature Module Pattern **Date**: 2025-01-15 **Status**: Accepted ## Context The Portal (Next.js frontend) needs a scalable organization pattern. Common approaches: - **File-type grouping**: All components in `/components`, all hooks in `/hooks` - **Feature grouping**: All billing code in `/features/billing` ## Decision Organize Portal code by **feature modules** with consistent internal structure: ``` apps/portal/src/features/billing/ ├── api/ # Data fetching (billing.api.ts) ├── hooks/ # React Query hooks (useBilling.ts) ├── stores/ # Zustand state (if needed) ├── components/ # Feature UI (InvoiceList.tsx) ├── views/ # Page-level views (InvoicesList.tsx) └── index.ts # Barrel exports ``` Pages in `app/` are thin wrappers that import views from features. ## Rationale ### Why Feature Modules? 1. **Cohesion**: All billing-related code is in one place. Need to modify billing? Look in `features/billing/`. 2. **Scalability**: Adding a new feature = adding a new folder. No need to touch multiple scattered directories. 3. **Clear ownership**: Easy to understand what a feature encompasses. 4. **Encapsulation**: Features export a public API via `index.ts`. Internal implementation details are hidden. ### Why Thin Pages? ```typescript // ✅ GOOD: Page is a thin wrapper // app/account/billing/invoices/page.tsx import { InvoicesListView } from "@/features/billing/views"; export default function InvoicesPage() { return ; } // ❌ BAD: Page contains logic export default function InvoicesPage() { const invoices = useInvoices(); // Data fetching in page return ; // Logic in page } ``` Benefits of thin pages: - Pages are declarative route definitions - Business logic is testable in isolation - Views can be reused across routes if needed ### Alternatives Considered | Approach | Pros | Cons | | ---------------------- | ----------------------- | ---------------------------------------- | | **File-type grouping** | Familiar, simple | Poor cohesion, hard to find related code | | **Domain-driven** | Clean boundaries | Overkill for frontend | | **Feature modules** | High cohesion, scalable | Requires discipline | ## Consequences ### Positive - High cohesion: all related code together - Easy to add new features - Clear public API per feature - Pages remain declarative ### Negative - Slightly more initial setup per feature - Requires consistent discipline across team ## Implementation ### Feature Module Structure ``` features/[feature-name]/ ├── api/ # Data fetching layer │ ├── [feature].api.ts # API service functions │ └── index.ts # Re-export ├── hooks/ # React Query hooks │ ├── use[Feature].ts # Primary hook │ └── index.ts ├── stores/ # Zustand stores (if needed) │ └── [feature].store.ts ├── components/ # Feature-specific UI │ ├── [Component].tsx │ └── index.ts ├── views/ # Page-level views │ └── [Feature]View.tsx ├── utils/ # Feature utilities └── index.ts # Public API (barrel export) ``` ### Example: Billing Feature ```typescript // features/billing/api/billing.api.ts export const billingService = { getInvoices: async (params) => apiClient.GET("/api/invoices", { params }), getInvoice: async (id) => apiClient.GET(`/api/invoices/${id}`), }; // features/billing/hooks/useBilling.ts export function useInvoices(params?: InvoiceQueryParams) { return useQuery({ queryKey: queryKeys.billing.invoices(params), queryFn: () => billingService.getInvoices(params), }); } // features/billing/views/InvoicesList.tsx export function InvoicesListView() { const { data: invoices } = useInvoices(); return ; } // features/billing/index.ts (public API) export { billingService } from "./api"; export { useInvoices, useInvoice } from "./hooks"; export { InvoicesListView } from "./views"; ``` ### Page Usage ```typescript // app/account/billing/invoices/page.tsx import { InvoicesListView } from "@/features/billing"; export default function InvoicesPage() { return ; } ``` ## Related - [Portal Architecture](../development/portal/architecture.md) - [ADR-006: Thin Controllers](./006-thin-controllers.md)