Frontend architecture
The best frontend architecture is the one you can still explain to a new hire six months in. Most over-architected frontends I’ve inherited were built by teams who confused “we might need this” with “we need this now.”
Monolith first. Extract when the map argues for it.
I start almost every new product as a Next.js monolith with an integrated API layer, PostgreSQL via Prisma, and cloud-managed services (Vercel, managed Postgres, S3) for the operational surface. No microservices. No BFF. No message queue unless the domain actually needs one.
Why: at the start of a product, the biggest risk is not “we’ll hit scale problems.” It’s “we’ll ship the wrong thing.” Every layer of architecture is a tax on iteration speed. A monolith lets me refactor across the seam in one PR instead of coordinating three deploys.
I design boundaries, not services. Feature folders, clean interfaces between UI and data, a domain layer that doesn’t know about React. When one of those boundaries starts to hurt — different scaling profile, different team, different deploy cadence — I extract that one thing. Not everything, one thing, when the pain is real.
The failure mode I’ve seen most: teams who split into microservices on day one because “we’re going to need it,” then spend eighteen months paying for coordination overhead on a product that never got past 200 users. Related: modernize without rewriting.
Folder convention I keep coming back to
Small variations per project, but the shape holds:
app/ Next.js App Router — routes only, thin (marketing)/ (app)/ dashboard/ orders/screens/ feature-scoped UI, one folder per screen dashboard/ components/ local to this screen hooks/ local hooks index.tsx the screen itselfcomponents/ genuinely global-shared UI onlylib/ domain logic, framework-agnostic orders/ types.ts validators.ts state-machine.tsserver/ API handlers, one file per actionhooks/ global-shared hooksThe rule: local until it isn’t. New code lives inside the screen
folder that uses it. Only when a second consumer appears does it move
to a shared location. This inverts the default (dump everything in
/components) and it keeps global folders from becoming
graveyards.
Design system as its own package — once it’s earning the split
Not always. For a single-product team under about ten engineers, extracting the design system before it’s causing pain is exactly the over-engineering I warned about three sections up. Keep it in the monorepo until something in the current arrangement is actually hurting.
When it earns the split: multiple apps sharing components, more than one team consuming it, or visual work being blocked by app-deploy cadence. Then a versioned package with its own storybook and its own release cycle is worth the overhead — visual tweaks stop risking the app, and app deploys stop risking the design system. Before that threshold, you’re maintaining two release processes to solve a problem that doesn’t exist yet.
The state-machine trap that isn’t a trap
Frontend state usually looks like “loading / loaded / error” and
that’s fine for read screens. Where it gets subtle is on interactive
flows — a wizard, a checkout, a scheduling UI. When I feel myself
reaching for useState for the fourth time in one component, that’s
the signal to reach for a reducer or an XState machine instead.
Not because reducers are trendy. Because the bug that’s coming is
“the button is enabled when it shouldn’t be” and the reducer makes
that structurally impossible.
Rendering strategy: choose per route, not per app
Next.js gives you SSR, SSG, ISR, RSC, and client-side rendering. You don’t have to pick one and impose it. Marketing pages are static, the dashboard is client-rendered behind auth, the admin panel is server-rendered with per-request data, the report is generated on demand and cached. The framework hands you a knob per route — use it.
Interactive visualization: one tool per concern
On the DP Productions real-estate viewer, we needed masterplan navigation, 3D building visualization, animated transitions, live property availability, and CRM data — all in one experience.
The wrong answer is “which one library does all this?” There isn’t one. Mapbox for geospatial rendering, Three.js for 3D, GSAP/Rive for motion, plain React Query for the business data. Each tool does the job it’s best at, and I do the glue. It’s more code than a single-tool solution, but every layer is legible and swappable. The alternative — one library stretched past what it was designed for — turns into an ownership problem the day that library stops being maintained.