Frontend architecture
Frontend architecture as a team-scaling problem
A 300-file src/components/ folder is unowned rather than messy. Grouping by technical kind (components/ utils/ helpers/) smears every feature across three folders, so every file is everyone's file and every change has global blast radius. One developer survives this fine. Four teams generate merge conflicts, accidental breakage, and a release train nobody dares to board. Conway's law runs in reverse here: when the code has no boundaries, the org's communication cost shows up as merge conflicts.
So the question is never "which folder structure is cleanest". It is which boundaries let N teams change code without talking to each other. Folders are the least interesting part of the answer. A folder is a claim about dependencies; without an import rule behind it, it is a comment, not architecture.
Layered architecture and Feature-Sliced Design
Every layered scheme rests on one sentence: imports point strictly downward. Domain-blind code (shared/: UI kit, lib wrappers, API client) sits at the bottom, business objects (entities/: product, user, order) above it, product capabilities (features/) above that, composition (pages/, app/) on top. Upward imports are the defect. The moment shared/format.ts imports a checkout type, everything consuming shared transitively depends on one feature, and since checkout already imports shared, you are one edge away from a cycle. "Shared" that knows about features is shared in name only.
Feature-Sliced Design is one formalization of this: layers (app → pages → widgets → features → entities → shared), slices by business domain within a layer, segments (ui / model / api / lib) within a slice. It has two teeth. Imports may come only from layers strictly below. And same-layer slices cannot import each other: a cross-feature need means the code belongs lower (an entity, the UI kit) or the composition belongs higher (a widget or page). This isolation keeps a feature deletable and ownable by exactly one team.
Be honest about the trade. FSD buys a shared vocabulary (new hires know where things go without asking), a rule mechanical enough for a linter, and pre-drawn ownership lines. It costs ceremony on small apps, recurring "is this a feature or an entity or a widget?" debates that burn real hours, and boilerplate per slice. It is overkill for a small team, for a library (FSD's own docs say it is for apps), and for any codebase whose current structure causes no pain; "we restructured to FSD" is not a win condition. The methodology matters less than the invariant: directional dependencies plus isolated, team-aligned slices, enforced by machine. Any scheme with those properties works. A perfect FSD tree with no lint rule rots back into the flat folder within two quarters.
How to enforce module boundaries
A slice boundary is real only if internals are private. The mechanism is the module system itself: one index.ts re-exports the contract, everything else is implementation.
// features/checkout/index.ts — the contract
export { CheckoutPanel } from "./ui/CheckoutPanel";
export { useCheckout } from "./model/useCheckout";
import { CheckoutPanel } from "@/features/checkout"; // ✅
import { computeTotals } from "@/features/checkout/model/totals"; // ❌ deep import — bypasses the contract
Deep imports are how boundaries die. Once another team reaches into your model/, your internals are frozen; you cannot rename a file without a cross-team breakage. So both rules go into lint, never into a README: eslint-plugin-boundaries or import/no-restricted-paths for layer direction and deep-import bans (Nx spells the same thing enforce-module-boundaries with project tags), plus dependency-cruiser or madge in CI for cycles.
Two caveats. Barrel files have a real cost: naive bundlers and test runners pull in the whole barrel graph, which hurts tree-shaking and startup, so keep barrels only at slice boundaries and off every other folder. And circular imports are not "handled by the bundler". ESM evaluates cycles with partially-initialized modules, so you get undefined bindings at init that surface as heisenbugs, and a cycle makes two modules refactorable only as a lump. Cycles metastasize: A↔B means anything touching A now touches B, which invites C into the knot. Zero cycles belongs in CI as a gate.
Ownership completes the picture. CODEOWNERS mapping each slice to its team turns "everyone reviews everything" into "Checkout reviews checkout". Boundary plus contract plus owner is the whole unit.
Monorepo vs polyrepo
First, kill the conflation: monorepo ≠ monolith ≠ single deploy. A monorepo is a repo strategy, many projects with explicit relationships and independent builds and deploys from one history. It buys atomic cross-project changes (a breaking API change and every consumer fix land in one PR, where polyrepo needs publish, bump, coordinate, pray), shared code as a folder instead of a published package with its own release process, one dependency version policy, and uniform tooling.
It costs infrastructure you now own. Naive colocation means every PR runs every test, so CI cost grows with the whole repo even for a one-line change. The tooling that makes monorepos viable at scale (Nx, Turborepo, Bazel-likes) does three things: a project graph (who depends on whom), affected-only execution (test and build only what a change reaches), and caching, including remote caching (never rebuild what a teammate already built). A monorepo proposal that skips affected-graph CI and remote caching is a proposal for the "big repo, 40-minute CI" failure mode. Add trunk discipline and CODEOWNERS, because the repo wall no longer provides ownership; lint boundaries and review routing have to.
Polyrepo's honest advantage is hard isolation: separate access control, separate compliance surfaces, teams that genuinely never share code. Its tax is that every shared change becomes a distributed transaction across package versions. Splitting the repo relocates the coordination cost into semver rather than removing it.
When micro-frontends make sense
Micro-frontends (independently built and deployed frontend apps composed at runtime) solve exactly one problem well: organizational decoupling. Teams that need their own release cadence (deploy Tuesday regardless of the platform team's freeze), their own stack (the acquired company ships Vue, you ship React), or a strangler path around a legacy frontend that cannot be rewritten in place. All three are org or legacy constraints; none is a code-quality goal.
Interviewers grade this question on whether you can itemize the bill:
- Payload. Independently compiled bundles duplicate dependencies, so the user downloads React once per MFE. Avoiding that means engineered sharing (module federation, import maps), and every shared singleton becomes a version contract enforced at runtime. MFE-B upgrading React ahead of the container stops being a package bump and becomes a production incident class.
- Coupling moves instead of dying. The shared design system and cross-app state (session, auth, cart) resurface as versioned runtime contracts between deployables. You wanted fewer coordination points and created ones that fail in production instead of in CI. Fowler's pragmatic advice: communicate through URLs and custom events, never shared mutable objects.
- Operational surface. N pipelines, N environments that drift from the composed production reality, integration testing that no single team owns, and global CSS conflicts that require discipline or shadow-DOM isolation.
The default answer for "many teams, one product" is a modular monolith in a monorepo: enforced slice boundaries give team autonomy, and one build gives one dependency graph, one design-system version, and failures that land in CI before they can land in production. Name the reversal condition too: with clean slice boundaries, extracting a genuinely deploy-independent unit later is cheap, so you adopt MFEs when the org constraint materializes rather than in advance. "Micro-frontends because we scaled past three teams" justifies nothing; "micro-frontends because Growth ships daily against a quarterly-release platform, and here is the sharing contract" is a complete answer.
Design system governance
Once multiple teams consume shared UI, the design system stops being a folder and becomes a product with an API surface, and its failure modes are governance failures. (How to design an individual component's API, headless components, the IoC ladder, slots: react-patterns territory. The question here is how forty components survive five consuming teams.)
Three decisions define it. Versioning and breaking changes: semver with a written policy. Deprecate with warnings for a window, ship a codemod for mechanical migrations, publish a changelog teams can act on. In a monorepo you can often skip versioning entirely and fix all consumers atomically in the breaking PR, which is the atomic-change benefit spent well; across repos or MFEs you are back to version skew, two Button versions rendering simultaneously in one product.
Contribution model: a central team is coherent but becomes a bottleneck, teams fork components while they wait, and the skew returns as copy-paste. Free-for-all turns the system into a junk drawer of one-off variants. The workable middle is federated contribution, where consuming teams open PRs and a small core team owns review, API conventions, and the deprecation calendar. Extension policy: decide explicitly what consumers may do when a component does not fit (wrap, compose, or contribute), because the undecided default is "copy it and diverge".
How to answer "how would you structure a large frontend"
Architecture interviews are lost by answering the question as asked. "How would you structure a large frontend?" has no answer on its own; the answer is a function of constraints, so extract them first: how many teams and who owns what (the biggest input); deploy cadence, one release train or independent shipping (this alone decides the MFE question); product shape, one coherent app, a suite, or a platform with legacy; existing pain, whether merge conflicts, build times, or onboarding. "It depends" is a red flag only when it stops there. "It depends on these four things, and here is the decision each one drives" is the senior signal.
Then present an evolution instead of an end-state: what ships in week one (lint boundaries plus rules for new code), what migrates by strangler behind a ratchet (legacy on an allowlist that only shrinks; its length is the progress metric), and what evidence triggers the next step. Name exit criteria, because "we restructured" is not an outcome: cross-team merge conflicts per week, cycle count at zero, review scope, affected-CI time. Say what you would refuse and what would change your mind: no micro-frontends until a deploy-cadence constraint is real, and no big-bang branch, ever.
beta
The interviewer part is in the works.
The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.