Assist_Design/docs/decisions/005-feature-module-pattern.md

154 lines
4.6 KiB
Markdown
Raw Normal View History

# 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 <InvoicesListView />;
}
// ❌ BAD: Page contains logic
export default function InvoicesPage() {
const invoices = useInvoices(); // Data fetching in page
return <InvoiceTable data={invoices} />; // 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 <InvoiceTable invoices={invoices} />;
}
// 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 <InvoicesListView />;
}
```
## Related
- [Portal Architecture](../development/portal/architecture.md)
- [ADR-006: Thin Controllers](./006-thin-controllers.md)