Scalable Directory Structures for Enterprise Web Apps

Structuring directory layouts in large-scale enterprise web applications requires moving beyond trivial framework defaults to architectures that promote modularity, maintainability, and domain encapsulation. As teams grow and codebases expand to hundreds of thousands of lines, traditional layer-first organizations (grouping purely by controllers, views, or services) fail due to high coupling and cognitive overhead. This guide outlines how enterprise engineering teams transition to feature-driven architectures, design self-contained modules, isolate shared logic, and use monorepos to scale applications efficiently.

The Pitfalls of Layer-First Grouping

Standard starter templates typically recommend grouping files by technical role:

src/
├── components/
├── controllers/
├── services/
├── views/
└── utils/

In large enterprise applications, this structure degrades quickly. Implementing a single business capability, such as updating user billing details, forces engineers to navigate half a dozen disparate folders. Discoverability declines, merge conflicts increase, and boundaries between unrelated business domains blur.

The Standard: Feature-First Architecture

Enterprise applications overwhelmingly favor a feature-first (or domain-driven) architecture. In this model, code is organized around business domains rather than technical responsibilities. Each domain acts as an autonomous module containing its own routing, UI, state, API integration, and tests.

src/
├── app/
│   ├── routes/
│   └── providers/
├── features/
│   ├── authentication/
│   │   ├── api/
│   │   ├── components/
│   │   ├── hooks/
│   │   ├── types/
│   │   └── index.ts
│   ├── billing/
│   │   ├── api/
│   │   ├── components/
│   │   ├── services/
│   │   ├── types/
│   │   └── index.ts
│   └── reporting/
├── shared/
│   ├── components/
│   ├── hooks/
│   ├── utils/
│   └── types/

Anatomical Breakdown of a Feature Module

Every feature directory represents a self-contained unit:

  • api/: Network requests, queries, mutations, and backend contract definitions specific to the feature.
  • components/: UI elements used exclusively within this specific domain.
  • hooks/ or services/: Business logic, state management, and orchestration.
  • types/: Domain models, schemas, and static type declarations.
  • index.ts (Public API): The entry point that explicitly defines what the feature exports to the rest of the application. Everything not exported here remains internal to the feature module.

Handling Cross-Cutting Concerns: Core vs. Shared

Enterprise layouts must explicitly define where code lives when it is not tied to a single feature. Engineers typically separate this into two distinct layers: core and shared.

The core/ Directory

The core directory holds global, singleton-like dependencies that are instantiated once at application startup. This logic is critical for app execution but is rarely touched during day-to-day feature development.

  • Global authentication interceptors and base HTTP clients.
  • Top-level error boundaries and logging infrastructure.
  • Route guards and base environment configurations.

The shared/ Directory

The shared (or common) directory contains reusable, stateless building blocks that any feature can import.

  • Primitive UI Components: Buttons, inputs, modals, and design-system tokens that contain zero domain knowledge.
  • Pure Utility Functions: Date formatters, math utilities, and validation helpers.
  • Global Types: Universal primitives shared across multiple bounded contexts.

Enterprise Monorepos and Workspace Layouts

When applications support multiple platforms (web, mobile, administrative portals), enterprises transition from a single monolithic directory to a workspace-based monorepo layout using tools like Turborepo, Nx, or pnpm workspaces.

repo-root/
├── apps/
│   ├── customer-portal/
│   │   ├── src/
│   │   └── package.json
│   └── admin-dashboard/
│       ├── src/
│       └── package.json
├── packages/
│   ├── ui-system/
│   │   ├── src/
│   │   └── package.json
│   ├── api-client/
│   │   ├── src/
│   │   └── package.json
│   ├── config-eslint/
│   └── tsconfig/
├── package.json
└── pnpm-workspace.yaml

This layout separates deployable targets (apps/) from reusable libraries (packages/). The UI system, analytics wrappers, and API client libraries are maintained independently with distinct dependency graphs, preventing duplicated logic across teams.

Enforcing Architectural Boundaries

A directory structure is only effective if its boundaries are mechanically enforced. Enterprise engineering teams implement strict automated safeguards to prevent layout degradation:

  1. Explicit Public Interfaces: Features must import from another feature's entry point (features/billing), never from its internal implementation details (features/billing/components/internal-card.tsx).
  2. Linting Rules: ESLint plugins such as eslint-plugin-boundaries or eslint-plugin-import enforce directional dependencies. For instance, code in shared/ is strictly prohibited from importing code from features/.
  3. Circular Dependency Detection: CI pipelines run automated checks to catch circular dependencies at build time, ensuring modules remain decoupled and independently testable.