File & Directory Structure
Organising code on disk so contributors can navigate without a map.
Overview
File and directory structure communicates the architecture of a codebase. Good structure makes it immediately clear where to find a given type of code, where to add new code, and what the relationship between modules is. The two primary organisation axes are feature-based (all code for a feature together) and layer-based (all controllers together, all models together).
Origin
Rails (DHH, 2004) popularised MVC-based layer structure (app/models, app/controllers, app/views) for web applications. The "screaming architecture" concept (Robert Martin, 2011) argued that directory names should reveal the domain, not the framework. Create React App established the src/ convention; Next.js 13 introduced the app/ directory with co-located server components.
Examples
Feature-based structure for a Next.js application
// Feature-based structure: each feature is self-contained
// src/
// features/
// orders/
// components/
// OrderCard.tsx
// OrderList.tsx
// hooks/
// useOrders.ts
// useOrderMutation.ts
// services/
// orderService.ts // API calls
// types/
// order.types.ts
// utils/
// orderFormatters.ts
// index.ts // public API of the feature
// auth/
// components/
// LoginForm.tsx
// hooks/
// useAuth.ts
// services/
// authService.ts
// shared/
// components/
// Button.tsx
// Modal.tsx
// hooks/
// useDebounce.ts
// utils/
// formatDate.ts
// app/ // Next.js 13 app directory
// (authenticated)/
// orders/
// page.tsx
// [id]/
// page.tsxFeature co-location means moving or deleting a feature is a single directory operation. The index.ts barrel file exports only the public API, preventing deep imports into the feature internals from other features.
Ruby on Rails service layer organisation
# app/
# models/ # ActiveRecord models (thin)
# services/ # Business logic, one class per operation
# orders/
# place_order_service.rb
# cancel_order_service.rb
# bulk_refund_service.rb
# payments/
# stripe_charge_service.rb
# refund_service.rb
# queries/ # Complex AR queries, extracted from models
# orders/
# pending_orders_query.rb
# orders_by_customer_query.rb
# presenters/ # View-layer transformations
# order_presenter.rb
# serializers/ # API JSON serialization
# order_serializer.rb
# jobs/ # Background jobs
# order_confirmation_email_job.rb
# Example: services follow the command pattern
class Orders::PlaceOrderService
def initialize(order_params:, customer:, payment_gateway: Stripe)
@order_params = order_params
@customer = customer
@payment_gateway = payment_gateway
end
def call
order = Order.create!(@order_params.merge(customer: @customer))
@payment_gateway.charge(order.total_cents, @customer.payment_method)
order
end
endExtracting services, queries, and presenters from models keeps models thin. The orders/ subdirectory groups all order-related services together. Each service is named after the operation (verb + noun), not the resource (noun alone).
Use Cases
- 01Large teams where engineers work on separate features; feature co-location minimises merge conflicts and makes ownership clear
- 02Monorepos where packages/feature-name directories group related frontend and backend code
- 03Domain-Driven Design implementations where directory structure mirrors Bounded Context boundaries
- 04Open source projects where contributors need to find the right file quickly; a clear structure reduces time to first contribution
When Not to Use
- //Do not prematurely adopt feature directories for a small project with 5-10 files; a flat src/ structure is simpler and easier to navigate
- //Do not create deeply nested directories for single files; a directory with one file is often better expressed as a module-level export
- //Do not use both feature-based and layer-based organisation in the same project; the inconsistency defeats the navigation benefit of either approach
Technical Notes
- TypeScript path aliases (tsconfig.json paths: { "@features/*": ["src/features/*"] }) eliminate deep relative imports (../../../../shared/utils) and make refactoring directory structure less disruptive
- Barrel files (index.ts re-exporting from a directory) improve import ergonomics but can cause circular dependency issues and slow TypeScript compilation in large codebases; use with awareness of these trade-offs
- Next.js 13 app directory uses filesystem-based routing with co-located components, server components, and route handlers; the structure enforces a specific pattern for each route
- Nx (monorepo tool) enforces module boundaries via lint rules: a feature library cannot import from another feature library, only from shared libraries. This prevents architectural drift in large monorepos