Home › Technical › Architecture

Architecture & scale patterns

The senior bar isn't writing code — it's making the architectural decisions that shape what the team writes for years. Stage 3 will press here. Have opinions. Have trade-offs ready.

01Frontend organization

How do you organize a large frontend codebase?

The two main axes:

  • By technical role (components/, hooks/, utils/) — easy at first, awful at scale. Touching one feature means hopping across folders.
  • By feature/domain (users/, orders/, inventory/) — each feature is a self-contained slice. Scales better.

For an enterprise app I'd lean feature-based, with a small shared/ or common/ layer for true cross-cutting code (UI primitives, types, utils). The discipline is: features can import from shared/, but features should rarely import from each other directly.

In a monorepo (Nx), this maps cleanly to libs/feature-orders, libs/feature-inventory, libs/ui, libs/utils.

What's a micro-frontend? When does it actually help?

Micro-frontends are the frontend version of microservices: split a big frontend into independently-deployable, independently-developed pieces, then compose them at runtime or build time.

Approaches:

  • Build-time composition — separate libs in a monorepo, composed via imports. Easiest. Often what people actually mean.
  • Module Federation (Webpack 5+) — load remote bundles at runtime. True independent deploys.
  • iframes — full isolation, big UX cost.
  • Edge composition — assemble at the CDN.

Real value: when you have multiple teams that need to ship independently, when you have legacy code you want to wall off, or when you have very different tech needs in different parts of the app.

Not value: small teams, single product, no compelling deployment-independence story. Micro-frontends bring real complexity (shared deps, design consistency, cross-app navigation) — they have to earn their cost.

For Omnesoft: their JD says "micro-frontend architecture using Nx." That's almost certainly Nx-style monorepo with module isolation, possibly with Module Federation. Worth probing in stage 3 — "what does micro-frontend mean to you guys today?"

How would you structure a design system / component library?

Layer the library:

  1. Tokens — colors, spacing, typography as CSS variables or a JS object. Single source of truth.
  2. Primitives — Button, Input, Select, Dialog. Styled with tokens. Behavior-correct (accessible, keyboard-navigable).
  3. Patterns / compounds — Card, FormField, DataTable. Built from primitives.
  4. Domain components — UserCard, OrderRow. Live in feature code, not the library.

Implementation choices to make explicitly:

  • Headless (Radix, Headless UI) + your own styling, vs. styled out-of-the-box
  • Variants via cva or similar, vs. ad-hoc props
  • Theming via CSS variables (best for runtime theming) vs. build-time tokens
  • Documentation in Storybook, with controls and a11y addons

02API & data layer

How do you keep frontend and backend types in sync?

Several options, in order of robustness:

  • OpenAPI / Swagger + generator (e.g. openapi-typescript) — generate TS types from the spec. Mature, language-agnostic.
  • tRPC — when both ends are TS. Direct type sharing. Beautiful when it fits; doesn't fit a .NET backend like Omnesoft's.
  • GraphQL + codegen — strict schema, generated client. Heavier but solid.
  • Hand-written types in a shared lib — works when you control both ends and the change rate is low. Gets out of sync without discipline.
  • Runtime validation with Zod / Valibot — type + parse on the wire. Catches drift the compiler can't.

For a .NET backend with FastEndpoints, OpenAPI is the natural fit. I'd assume Omnesoft does this and ask to confirm.

How do you handle authentication on the frontend?

Standard pattern for a modern SPA:

  • Tokens or session cookies — for SPAs, prefer HTTP-only secure cookies if your backend is on a related domain. Less XSS exposure than localStorage.
  • OAuth / OIDC for SSO flows — Auth0, Azure AD, Okta. Use the official SDK; don't roll your own.
  • Refresh strategy — silent refresh in the background before the access token expires. Handle the 401 case with a refresh-and-retry interceptor.
  • Logout — clear local state, revoke tokens server-side, navigate to a public route.
  • Route guards — wrap protected routes in an auth provider that redirects when unauthenticated.

Things I avoid: storing tokens in localStorage if the app has user-generated content (XSS surface), passing tokens in URLs, leaking them in logs or error reports.

How do you handle errors across the frontend?

Multiple layers:

  1. API errors — typed error contracts from the backend, surfaced via TanStack Query's error state. Standard handling for common cases (401 → re-auth, 403 → forbidden UI, 5xx → retry with backoff).
  2. Component errors — Error Boundaries around feature regions, with a graceful fallback and reporting to Sentry / App Insights.
  3. Form validation errors — handled at the form level with the validation library (zod, react-hook-form).
  4. Background errors — async work that doesn't have UI: log to monitoring, show a toast if the user should know.

The discipline is: every error has a designed home. "It just throws somewhere" is a smell.

03State architecture

How do you decide where state lives?

I think in three categories:

  • Server state — fetched data. TanStack Query handles cache, invalidation, refetching, optimistic updates. Don't put server data in your client state store.
  • URL state — anything shareable. Filters, selected tab, pagination, modal open/closed if it should be deep-linkable. Lives in the URL via the router.
  • Client state — UI-only. Modal open (if not URL-linkable), drag-in-progress, hover, transient form input. Local component state by default; lift only when needed.

The mistake I see most: putting URL state in client state ("which tab is selected" in useState), losing the back-button and shareable-link behavior.

How would you architect state for a complex form?

For anything beyond a few fields: react-hook-form + zod.

  • react-hook-form keeps each input uncontrolled, so re-renders are scoped — performant for big forms.
  • zod schema for validation, single source of truth for shape and rules.
  • Schema doubles as TypeScript type — z.infer<typeof schema>.

For dynamic forms (variable fields based on prior answers): pair RHF with useFieldArray and conditional rendering driven by watch.

For multi-step / wizard forms: keep step state in URL or a parent reducer; submit each step independently if the backend supports it; persist drafts to localStorage if the form is long.

04Routing & navigation

How do you handle client-side routing?

For React: React Router (now data-router style) or TanStack Router. TanStack Router is great if you're already in the TanStack ecosystem and want full type-safe routes.

Capabilities the router needs to support:

  • Nested layouts
  • Loaders that run before render (data fetching co-located with the route)
  • Route-level code splitting
  • Protected routes with auth guards
  • Search params as typed state
  • Scroll restoration

05Build & deploy

Vite vs webpack vs esbuild?
  • Vite — modern default. ESM-native dev server (no bundling in dev), Rollup for production. Fast, ergonomic. What I'd choose for a new project.
  • webpack — battle-tested, every plugin under the sun, Module Federation. What you live with on legacy enterprise apps.
  • esbuild / swc — extremely fast tools, often used by the others (Vite uses esbuild for transforms; Next has used swc). You rarely use them directly as the top-level build tool.
  • Turbopack — Next.js's bet. Maturing.

For Omnesoft's stack (React + TS + Tailwind + Nx), Vite is the natural fit. Worth confirming what they use.

What does a CI pipeline for a frontend look like?

Standard senior-level pipeline (GitHub Actions, since they call it out):

  1. Install with cache (pnpm/npm/yarn cache hit ratio matters at scale)
  2. Lint (eslint, prettier --check)
  3. Type check (tsc --noEmit)
  4. Unit + integration tests (jest / vitest)
  5. Build production bundle
  6. Bundle-size check (size-limit, fail if over budget)
  7. E2E tests (Playwright) — usually a smoke set on every PR, full suite on main
  8. Lighthouse CI — track Web Vitals against a budget
  9. Deploy preview to a per-PR URL

For Nx, affected commands are the trick — only run lint/test/build on what changed, dramatically reducing CI time as the monorepo grows.

06Cross-cutting concerns

How do you handle internationalization (i18n)?

This is your home turf — translation infra is exactly what you've worked on at Artlist. Speak from experience here.

The fundamentals:

  • Library: react-i18next, FormatJS / react-intl, or framework-native (Next.js i18n).
  • Storage: JSON locale files, organized by feature/route to enable splitting.
  • Loading strategy: don't ship every locale to every user. Split by route boundary, lazy-load the user's locale.
  • Detect dead keys; have CI fail when adding a key without an English value.
  • Pluralization and ICU MessageFormat for languages with complex grammar rules — don't try to hand-roll this.
  • RTL support — designed in from the start with logical CSS properties (margin-inline-start, etc.) instead of left/right.
How do you handle feature flags?

Three layers:

  • Build-time flags — env vars, baked into the bundle. Cheapest, no runtime cost, but a deploy is required to change.
  • Runtime flags — fetched from a service (LaunchDarkly, Unleash, or a homegrown one). Can target users, roll out gradually, kill instantly.
  • URL-based / query-param flags for QA and dogfooding.

Discipline: flags accumulate. Have a process to retire them. A flag that's been fully on for a year is technical debt.

07System design exercise (likely in stage 3)

If they ask "how would you design X" — typical prompts at senior level:

  • "Design the frontend for an inventory dashboard with 100k items, real-time updates."
  • "Design the architecture for a multi-tenant SaaS frontend."
  • "How would you migrate a large legacy frontend to a new framework gradually?"

Process to follow out loud:

  1. Clarify requirements — load characteristics, user count, real-time needs, offline?
  2. Talk trade-offs — list the 2-3 main architectural choices, what each gives up
  3. Pick one and justify
  4. Sketch the data flow — fetching, caching, invalidation, real-time channel
  5. Address the hard parts — perf at scale, error states, observability
  6. Acknowledge what you'd skip — "I'd defer X until we see traffic patterns"