Skip to the solution
MediumLow-Level Design6 min read

How would you structure a large React application?

1.1k views

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 structure
  • architecture
  • scalability
  • organization

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

6 min read Β· 9 sections

Try it in the playground

Open any example in the playground to run and change it.

ReactThe two layouts side by side, with the import rules that keep one healthy
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>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 35 of 119 in React.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All React.js questions
Up nextMedium

What are render props and how do they work?

Continue