Skip to solution
mediumLow-Level Design

How would you structure a large React application?

1.1k views
01

Understand the problem

Question presented to candidate: "You are starting a codebase several teams will work in for years. How do you organise it?"

What a strong answer should cover:

  • Organise by feature, not by file type. Grouping every component in /components and every hook in /hooks means one change touches five folders and nothing is co-located.
  • A feature folder owns its components, hooks, types, tests and data access, and exposes a deliberate public surface — usually a single index — so its internals stay internal.
  • Dependency direction is the real architecture: features may depend on shared, shared may never depend on a feature, and features should not reach into each other's internals.
  • Enforce it with tooling. Import-boundary lint rules turn a convention into a check; without them the structure decays under deadline pressure.
  • Colocation beats categorisation — a component's test, styles and types belong beside it, so deleting the feature deletes all of it.
  • Keep a genuine shared layer for design-system components and cross-cutting utilities, and be strict about entry: shared code is code that is genuinely reusable, not code nobody knew where to put.
  • State placement: server state in a data-fetching layer, URL state in the URL, local UI state in the component. A global store is for the little that is genuinely global.
  • Structure serves change: the test is whether a new engineer can find, modify and delete a feature confidently.

Clarifying questions expected:

  • "How many teams, and will they own separate areas?" — that changes how hard the boundaries need to be.
  • "Is there a framework with its own routing conventions?" — that constrains the top level.

Code / implementation expected: No. A folder tree and the dependency rules are the answer.

project structurearchitecturescalabilityorganization
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for senior React interviews — assumes practical experience. Difficulty: Medium

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. A note on verification: this is an architecture question with no

Solution ready — 2 min read

Classified // press E to declassify

04

Explore the playground snippets

The two layouts side by side, with the import rules that keep one healthy
Run Playground
import { useState } from "react";

const BY_TYPE = `src/
  components/
    CheckoutForm.tsx        <- checkout
    OrderSummary.tsx        <- checkout
    ProductCard.tsx         <- catalogue
    Button.tsx              <- genuinely shared
  hooks/
    useCheckout.ts          <- checkout
    useProducts.ts          <- catalogue
  api/
    checkout.ts             <- checkout
    products.ts             <- catalogue
  types/
    checkout.ts             <- checkout
    product.ts              <- catalogue
  __tests__/
    CheckoutForm.test.tsx   <- checkout

# "Delete the checkout feature" means finding SIX files across five folders,
# and nothing in the tree tells you which ones belong together.`;

const BY_FEATURE = `src/
  features/
    checkout/
      CheckoutForm.tsx
      OrderSummary.tsx
      useCheckout.ts
      checkout.api.ts
      checkout.types.ts
      CheckoutForm.test.tsx
      index.ts              <- the ONLY thing other features may import
    catalogue/
      ProductCard.tsx
      useProducts.ts
      products.api.ts
      index.ts
  shared/
    ui/Button.tsx           <- design system
    lib/formatMoney.ts      <- genuine utility
  app/
    routes.tsx

# "Delete the checkout feature" means deleting one folder.`;

const RULES = [
  ["features/* → shared/*", true, "the normal direction"],
  ["features/checkout → features/catalogue (via index)", null, "allowed, but a smell if frequent"],
  ["features/checkout → features/catalogue/useProducts", false, "deep import past the public surface"],
  ["shared/* → features/*", false, "inverts the dependency; shared becomes unmovable"],
  ["features/* → features/* (circular)", false, "usually a missing shared concept"],
];

export default function App() {
  const [feature, setFeature] = useState(true);

  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6, maxWidth: 620 }}>
      <div style={{ display: "flex", gap: 8, marginBottom: 10 }}>
        <button onClick={() => setFeature(false)} style={{ fontWeight: !feature ? "bold" : "normal" }}>
          ❌ by file type
        </button>
        <button onClick={() => setFeature(true)} style={{ fontWeight: feature ? "bold" : "normal" }}>
          ✅ by feature
        </button>
      </div>

      <pre style={{ background: feature ? "#f2f9f2" : "#fdf0f0", border: "1px solid #ddd",
                    borderRadius: 8, padding: 12, fontSize: 12, overflowX: "auto", whiteSpace: "pre-wrap" }}>
{feature ? BY_FEATURE : BY_TYPE}
      </pre>

      <h4 style={{ margin: "14px 0 6px" }}>The import rules — enforce these with lint, not a README</h4>
      <table style={{ width: "100%", fontSize: 12, borderCollapse: "collapse" }}>
        <tbody>
          {RULES.map(([rule, ok, why]) => (
            <tr key={rule} style={{ borderBottom: "1px solid #eee" }}>
              <td style={{ padding: "3px 8px 3px 0", fontFamily: "ui-monospace, monospace" }}>{rule}</td>
              <td style={{ color: ok === true ? "#161" : ok === false ? "#a33" : "#a60", whiteSpace: "nowrap" }}>
                {ok === true ? "allow" : ok === false ? "forbid" : "allow, watch"}
              </td>
              <td style={{ color: "#666" }}>{why}</td>
            </tr>
          ))}
        </tbody>
      </table>

      <p style={{ fontSize: 13, color: "#666" }}>
        The folder layout is the visible half; the import rules are the half
        that actually holds. Without a lint rule forbidding deep imports and
        shared-to-feature imports, both layouts converge on the same tangle
        within a year — the second one just takes slightly longer.
      </p>
    </div>
  );
}
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 35 of 119 decoded in the React.js track. One more won't hurt.

Back to track