I’m not a designer, I’m an engineer, so I wanted a clean look and feel and used ready-made components where MUI fit nicely. Tailwind filled in whatever gaps were left, mostly spacing and one-off layout tweaks that didn’t warrant a full MUI component. This gave me time to focus on the structure and the application logic instead of building a design system from scratch.
Feature-Based Structure
The frontend is organized by feature, not by type. Each domain gets its own folder under features/, instead of one shared components/ or hooks/ for the whole app:
src/features/
├── transactions/
├── accounts/
├── budgets/
├── category/
├── dashboard/
├── family/
├── imports/
├── reports/
├── settings/
├── auth/
└── app/
Every feature folder repeats the same internal shape: api/, components/, hooks/, pages/, and sometimes types/. transactions is a good example of it in full:
src/features/transactions/
├── api/
│ └── transactionsApi.ts
├── components/
│ ├── AddTransactionModal.tsx
│ ├── BulkActions.tsx
│ ├── TransactionForm.tsx
│ └── index.ts
├── hooks/
│ ├── useCreateTransaction.ts
│ ├── useDeleteTransaction.ts
│ ├── useTransaction.ts
│ ├── useTransactions.ts
│ └── useUpdateTransaction.ts
├── pages/
│ ├── AddTransactionPage.tsx
│ ├── ImportCsvPage.tsx
│ ├── TransactionDetailPage.tsx
│ ├── TransactionsPage.tsx
│ └── index.ts
└── types/
└── index.ts
That matched how I worked: feature by feature, one slice at a time. It also made it easier to steer Claude Code, reviewing and merging a whole feature end-to-end before moving to the next, instead of reviewing partial changes scattered across shared components/ or hooks/ directories.
Tenant-Scoped Caching
Every entity in Pocket Family belongs to a family (tenant), and I wanted that scoping present in the cache too, not just in the API calls. TanStack Query hooks build their query key from the family ID plus whatever filters are active:
queryKey: familyId ? ['transactions', familyId, filters] : ['transactions', 'all', filters]
- Each family gets its own cache entries, so switching families in the family switcher doesn’t need a hard refetch or risk showing another family’s data from a stale cache.
- Filters are part of the key too. Each distinct filter combination is its own cache entry, so returning to a filter you’ve already applied is instant.
- The
'all'case is the deliberate exception, covering cross-family views like “all accounts,” not an oversight where scoping got missed.
React vs. Angular
The most notable framework decision was React vs. Angular, made right at the start since I needed to learn whichever one I picked. I chose React because it’s less opinionated: it doesn’t ship with routing, forms, or state management built in, so building the app meant building and wiring up all of that myself. That was the point, I wanted the broader frontend scope, not a narrower, more guided one. Angular is widely used and more batteries-included, but it would have meant learning its conventions instead of the ecosystem underneath it.
The real cost is that React alone doesn’t give you the structure Angular does out of the box. Every decision, from routing (React Router) to forms (react-hook-form) to server state (TanStack Query), had to be made and wired together by hand instead of coming pre-decided. That’s more surface area to get wrong, but it’s also exactly the range I wanted to build.
Test Suite Performance
If I had to pick one area to improve across the whole project, it’s this one. The Vitest suite is slow, and running tests in parallel caused timeouts, race conditions, and resource contention that took a long time to shake out. It was one of the hardest things to get Claude to fix well, most attempts fixed one flaky test and pushed the problem onto another. It’s reliable and stable now, good enough to trust, but the underlying speed problem is managed rather than solved.