Overview
Directory structure refers to the hierarchical organization of files and folders within a software project. This organization determines how source code, tests, configuration files, documentation, and assets are arranged on the filesystem. A well-designed directory structure makes codebases navigable, maintainable, and understandable for developers joining a project.
Directory structures serve multiple purposes. They establish conventions that reduce cognitive load when navigating unfamiliar code. They separate concerns by grouping related functionality. They support build systems and deployment pipelines that expect files in specific locations. They communicate architectural decisions through physical organization.
The choice of directory structure impacts development velocity, onboarding time, and long-term maintainability. A structure that aligns with team workflow and project architecture reduces friction. A structure that conflicts with these factors creates constant resistance.
project/
├── lib/ # Library code
├── test/ # Test files
├── bin/ # Executables
├── config/ # Configuration
└── docs/ # Documentation
Most programming languages and frameworks establish conventional directory structures. Ruby projects typically follow patterns influenced by RubyGems and Rails. These conventions create shared expectations across the Ruby community, making codebases immediately familiar to experienced Ruby developers.
Key Principles
Separation of Concerns: Directory structures physically separate different types of code and resources. Production code lives separately from tests. Configuration files separate from application logic. This separation prevents accidental coupling and makes it clear where to find specific functionality.
Convention Over Configuration: Established conventions reduce the need for explicit configuration. When files follow expected patterns, tools and frameworks locate them automatically. Rails exemplifies this principle—models go in app/models, controllers in app/controllers. This convention eliminates configuration boilerplate.
Depth vs. Breadth Trade-off: Directory hierarchies balance depth (nested folders) against breadth (many files in one folder). Deep hierarchies provide organization but increase navigation cost. Shallow hierarchies simplify navigation but create crowded folders. The optimal balance depends on project size and complexity.
Namespace Mapping: Directory structure often mirrors code namespace structure. A class Company::Billing::Invoice typically resides at lib/company/billing/invoice.rb. This mapping makes files predictable to locate and maintains consistency between logical and physical organization.
Build System Integration: Build tools, test runners, and deployment scripts rely on directory structure. Files in specific locations trigger specific processing. Ruby gems expect a lib directory for source code and a bin directory for executables. Rails expects assets in specific folders for the asset pipeline.
Team Workflow Alignment: Directory structure should match how teams work. Feature-based structures group all code for a feature together. Layer-based structures separate presentation, business logic, and data access. The structure should minimize merge conflicts and support parallel development.
Discoverability: Developers should locate files without extensive searching. Standard locations for common file types reduce navigation time. Clear naming and logical grouping make browsing the codebase intuitive. Documentation and README files at appropriate levels provide context.
Scalability: Directory structures must accommodate growth. A structure that works for 50 files may fail at 5,000 files. Hierarchical organization becomes more important as projects grow. Modular structures that support extracting subsystems into separate repositories provide long-term flexibility.
Ruby Implementation
Ruby and its ecosystem establish strong conventions for directory structure. These conventions differ between standalone Ruby projects, gems, and Rails applications.
Standard Ruby Project Structure: Ruby projects without frameworks follow minimal conventions. The lib directory contains library code, with the main entry point matching the project name:
# lib/payment_processor.rb
module PaymentProcessor
VERSION = "1.0.0"
end
# lib/payment_processor/transaction.rb
module PaymentProcessor
class Transaction
def initialize(amount)
@amount = amount
end
end
end
Directory structure mirrors the namespace hierarchy. A PaymentProcessor::CreditCard::Validator class resides at lib/payment_processor/credit_card/validator.rb.
RubyGems Directory Convention: Gems follow a standardized structure that Bundler and RubyGems recognize:
my_gem/
├── lib/
│ ├── my_gem.rb # Main entry point
│ └── my_gem/
│ ├── version.rb
│ └── core.rb
├── test/ or spec/ # Tests
├── bin/ # Executables
├── my_gem.gemspec # Gem specification
├── Gemfile # Dependencies
└── README.md
The lib/my_gem.rb file serves as the entry point that requires other files. This convention allows require 'my_gem' to load the entire gem.
Rails Directory Structure: Rails applications follow an opinionated Model-View-Controller structure:
# app/models/user.rb
class User < ApplicationRecord
has_many :orders
end
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def index
@users = User.all
end
end
# app/views/users/index.html.erb
<% @users.each do |user| %>
<%= user.name %>
<% end %>
Rails autoloading maps directory structure to constant names. A file at app/models/billing/invoice.rb defines Billing::Invoice. This autoloading eliminates explicit require statements within the application.
Custom Autoload Paths: Rails projects can configure additional autoload paths for custom organization:
# config/application.rb
module MyApp
class Application < Rails::Application
config.autoload_paths += %W(
#{config.root}/lib
#{config.root}/app/services
#{config.root}/app/queries
)
end
end
This configuration allows organizing code beyond standard Rails directories while maintaining autoloading conventions.
Zeitwerk Loader Integration: Modern Rails uses Zeitwerk for autoloading, which enforces strict file-to-constant mapping:
# lib/payment_gateway.rb must define PaymentGateway
module PaymentGateway
end
# lib/payment_gateway/client.rb must define PaymentGateway::Client
module PaymentGateway
class Client
end
end
Zeitwerk raises errors when filenames don't match constant names, preventing subtle bugs from mismatched organization.
Engine and Component Isolation: Rails engines create isolated directory structures within a larger application:
engines/
├── billing/
│ ├── app/
│ │ ├── models/billing/
│ │ └── controllers/billing/
│ └── lib/billing/
└── inventory/
├── app/
└── lib/
Each engine maintains its own directory structure, allowing modular development while remaining part of the parent application.
Common Patterns
Feature-Based Organization: Grouping all files related to a feature creates cohesive modules. This pattern works well for large applications where features operate independently:
app/
├── authentication/
│ ├── models/
│ │ ├── user.rb
│ │ └── session.rb
│ ├── controllers/
│ │ └── sessions_controller.rb
│ └── services/
│ └── authenticator.rb
└── billing/
├── models/
│ ├── invoice.rb
│ └── payment.rb
└── services/
└── charge_processor.rb
Feature-based structure supports parallel development—different teams work in different feature directories with minimal conflicts. Extracting a feature into a separate service or gem becomes straightforward.
Layer-Based Organization: Traditional MVC applications organize by technical layer. Rails follows this pattern by default:
app/
├── models/
│ ├── user.rb
│ ├── order.rb
│ └── product.rb
├── controllers/
│ ├── users_controller.rb
│ └── orders_controller.rb
└── views/
├── users/
└── orders/
Layer-based structure makes technical roles clear and aligns with framework conventions. Finding all models or all controllers requires checking only one directory.
Service Object Pattern: Extracting business logic into service objects creates a dedicated directory:
# app/services/order_placement.rb
class OrderPlacement
def initialize(user, cart)
@user = user
@cart = cart
end
def call
ActiveRecord::Base.transaction do
create_order
charge_payment
send_confirmation
end
end
private
def create_order
Order.create!(user: @user, items: @cart.items)
end
end
Service objects reside in app/services with one class per file. Complex services may have subdirectories: app/services/billing/, app/services/inventory/.
Domain-Driven Design Structure: DDD projects organize by domain concepts, with each domain containing its own layers:
domains/
├── ordering/
│ ├── entities/
│ │ ├── order.rb
│ │ └── order_line.rb
│ ├── repositories/
│ │ └── order_repository.rb
│ └── services/
│ └── order_fulfillment.rb
└── catalog/
├── entities/
│ └── product.rb
└── repositories/
└── product_repository.rb
This structure enforces domain boundaries and makes dependencies explicit. Cross-domain interaction happens through defined interfaces.
Gem-Style Library Organization: Projects that serve as libraries follow gem conventions even within larger applications:
lib/
├── payment_gateway.rb
├── payment_gateway/
│ ├── client.rb
│ ├── transaction.rb
│ └── errors.rb
└── payment_gateway/
└── adapters/
├── stripe.rb
└── braintree.rb
This organization supports extracting the library into a gem later. The structure remains consistent whether code exists in a gem or within an application.
Test Directory Mirroring: Test files mirror the structure of the code they test:
lib/
└── payment_processor/
├── charge.rb
└── refund.rb
test/
└── payment_processor/
├── charge_test.rb
└── refund_test.rb
Mirrored structure makes finding tests trivial. Developers locate test files by applying a simple transformation to the source file path.
Practical Examples
Monolithic Rails Application Structure: A mature Rails application balances convention with custom organization:
app/
├── assets/
│ ├── images/
│ ├── stylesheets/
│ └── javascripts/
├── controllers/
│ ├── api/
│ │ └── v1/
│ │ ├── users_controller.rb
│ │ └── orders_controller.rb
│ └── admin/
│ └── dashboard_controller.rb
├── models/
│ ├── concerns/
│ │ ├── searchable.rb
│ │ └── auditable.rb
│ ├── user.rb
│ └── order.rb
├── services/
│ ├── order_fulfillment.rb
│ └── inventory_sync.rb
├── queries/
│ └── sales_report_query.rb
└── views/
├── layouts/
└── users/
This structure extends Rails conventions with services, queries, and API versioning. Concerns provide shared behavior without cluttering model directories.
Microservice Repository Structure: A service-oriented repository isolates each service:
services/
├── user_service/
│ ├── app/
│ │ ├── models/
│ │ └── controllers/
│ ├── config/
│ ├── Gemfile
│ └── README.md
├── order_service/
│ ├── app/
│ ├── config/
│ └── Gemfile
└── shared/
└── lib/
├── messaging/
└── logging/
Each service operates as an independent application with its own dependencies. The shared directory contains code used across services, potentially packaged as internal gems.
Component-Based Frontend Structure: Rails applications with complex frontend code separate assets by component:
app/
├── components/
│ ├── button/
│ │ ├── button.rb
│ │ ├── button.html.erb
│ │ └── button.css
│ └── modal/
│ ├── modal.rb
│ ├── modal.html.erb
│ └── modal.js
└── views/
└── pages/
└── home.html.erb
Component directories colocate Ruby logic, templates, styles, and JavaScript. This organization supports frameworks like ViewComponent or Phlex.
Gem Development Structure: A production gem includes additional directories for development and documentation:
# lib/awesome_gem.rb
require 'awesome_gem/version'
require 'awesome_gem/configuration'
require 'awesome_gem/client'
module AwesomeGem
class << self
attr_accessor :configuration
end
def self.configure
self.configuration ||= Configuration.new
yield(configuration)
end
end
awesome_gem/
├── lib/
│ ├── awesome_gem.rb
│ └── awesome_gem/
│ ├── version.rb
│ ├── configuration.rb
│ └── client.rb
├── spec/
│ ├── spec_helper.rb
│ └── awesome_gem/
│ └── client_spec.rb
├── bin/
│ ├── console
│ └── setup
├── docs/
│ ├── getting_started.md
│ └── api_reference.md
├── examples/
│ └── basic_usage.rb
└── awesome_gem.gemspec
The bin/console script provides an IRB session with the gem loaded. The examples directory demonstrates usage patterns. Comprehensive documentation lives in docs.
Monorepo Structure: A monorepo contains multiple related projects:
monorepo/
├── gems/
│ ├── core/
│ │ ├── lib/
│ │ └── core.gemspec
│ └── client/
│ ├── lib/
│ └── client.gemspec
├── services/
│ ├── api/
│ └── worker/
├── shared/
│ └── config/
└── scripts/
├── bootstrap
└── test
This structure shares code through local gems while maintaining separate services. Build scripts in the root coordinate across projects. The shared directory contains configuration used by all projects.
Design Considerations
Project Size Impact: Small projects benefit from flat structures with minimal nesting. A Ruby script with supporting files needs only a lib directory and a test directory. Medium projects require more organization—separate directories for models, services, and utilities. Large projects demand hierarchical structures with clear domain boundaries.
Team Structure Alignment: Directory structure should match team organization. Teams working on separate features benefit from feature-based directories. Teams organized by technical expertise work better with layer-based structures. Cross-functional teams building vertical slices need directories that support end-to-end feature development.
Framework Constraints: Frameworks impose directory requirements. Rails expects specific directories for autoloading to function. Violating these conventions requires configuration that undermines framework benefits. Projects using frameworks should adapt structure to framework expectations rather than fighting conventions.
Modularity Requirements: Projects planning to extract components into separate repositories need modular structures. Extracting a Rails engine requires isolated directory structure from the start. Building a gem within a larger application requires gem-compatible structure. Anticipating extraction simplifies the process when modularity becomes necessary.
Testing Strategy: Test directory structure depends on testing approach. Unit tests often mirror source structure. Integration tests may group by feature or user journey. Test fixtures and factories need dedicated directories. Large test suites benefit from helper modules organized separately.
Deployment Model: Deployment approach influences directory structure. Applications deployed as containers may separate runtime code from build-time dependencies. Applications deployed to multiple environments may separate environment-specific configuration. Serverless deployments may require function-specific directories.
Autoloading vs. Explicit Loading: Rails autoloading allows casual directory organization—files load automatically based on constant names. Projects without autoloading need explicit require statements and more careful structure. Gem development requires explicit requires in the main entry point, influencing how files organize.
Shared Code Management: Projects sharing code across multiple applications need strategies for shared directories. Internal gems provide clean separation but add complexity. Shared directories within a monorepo simplify development but create coupling. Symlinks offer another option but complicate deployment.
Migration Path: Evolving directory structure requires careful migration. Moving files breaks requires in projects without autoloading. Renaming directories containing many files creates large pull requests. Gradual migration strategies—moving subsystems incrementally—reduce disruption while improving organization.
Build Performance: Directory structure affects build performance. Deep hierarchies slow filesystem operations. Excessive files in one directory slow directory listing. Test file location impacts test selection performance. These factors matter most in large projects with long build times.
Tools & Ecosystem
Rails Generators: Rails provides generators that create files in conventional locations:
rails generate model User name:string email:string
# Creates app/models/user.rb and db/migrate/[timestamp]_create_users.rb
rails generate controller Users index show
# Creates app/controllers/users_controller.rb and app/views/users/
rails generate scaffold Product name:string price:decimal
# Creates model, controller, views, and migration
Generators maintain structure consistency across team members. Custom generators can enforce project-specific conventions.
Bundler Gem Structure: Bundler's bundle gem command creates standard gem directory structure:
bundle gem payment_processor
# Creates:
# lib/payment_processor.rb
# lib/payment_processor/version.rb
# payment_processor.gemspec
# spec/spec_helper.rb
# README.md
# LICENSE.txt
This structure follows RubyGems conventions and includes necessary metadata files. The generated structure works immediately with Bundler and RubyGems.
RuboCop Configuration: RuboCop enforces directory naming conventions:
# .rubocop.yml
Naming/FileName:
Enabled: true
ExpectMatchingDefinition: true
Rails/ApplicationController:
Enabled: true
Rails/ApplicationRecord:
Enabled: true
The ExpectMatchingDefinition setting ensures file names match constant names. Rails-specific cops enforce framework conventions.
Zeitwerk Loader: Zeitwerk provides strict autoloading that enforces directory-to-constant mapping:
# config/application.rb
config.autoloader = :zeitwerk
# Zeitwerk requires:
# lib/payment_gateway.rb defines PaymentGateway
# lib/payment_gateway/client.rb defines PaymentGateway::Client
Zeitwerk's zeitwerk check command validates directory structure matches expected constants, catching mismatches before runtime.
Rails Engines: Engines provide isolated directory structures within Rails applications:
# lib/billing/engine.rb
module Billing
class Engine < ::Rails::Engine
isolate_namespace Billing
end
end
rails plugin new billing --mountable
# Creates engine directory structure with isolated namespace
Engines maintain their own app, config, and lib directories, supporting modular development within a single repository.
Component Frameworks: ViewComponent and similar frameworks provide component organization:
# app/components/button_component.rb
class ButtonComponent < ViewComponent::Base
def initialize(text:, style: :primary)
@text = text
@style = style
end
end
# app/components/button_component.html.erb
<button class="btn btn-<%= @style %>">
<%= @text %>
</button>
Component frameworks encourage organizing views, Ruby logic, and tests together in component directories.
Directory Structure Linters: Custom tools can enforce project-specific directory conventions:
# scripts/check_structure.rb
REQUIRED_DIRS = %w[app lib config test].freeze
FORBIDDEN_PATTERNS = [
/app\/models\/concerns\/.*\/.+/, # No nested concerns
/lib\/[^\/]+\/[^\/]+\/[^\/]+\/.+/ # Max 3 levels in lib
].freeze
REQUIRED_DIRS.each do |dir|
abort "Missing required directory: #{dir}" unless Dir.exist?(dir)
end
Dir.glob('**/*.rb').each do |file|
FORBIDDEN_PATTERNS.each do |pattern|
abort "Forbidden structure: #{file}" if file.match?(pattern)
end
end
Custom linters codify team conventions and prevent structure violations during development.
Reference
Standard Ruby Project Directories
| Directory | Purpose | Contents |
|---|---|---|
| lib | Library source code | Main module definition and implementation files |
| bin | Executable scripts | Command-line tools and entry points |
| test or spec | Test files | Unit tests, integration tests, test helpers |
| config | Configuration files | YAML configs, environment settings |
| docs | Documentation | API documentation, guides, examples |
| examples | Usage examples | Sample code demonstrating library usage |
Rails Application Directories
| Directory | Purpose | Contents |
|---|---|---|
| app/models | Data models | ActiveRecord models and business objects |
| app/controllers | Request handlers | Controllers managing HTTP requests |
| app/views | Templates | ERB, HAML, or other view templates |
| app/helpers | View helpers | Methods supporting view rendering |
| app/mailers | Email handlers | ActionMailer classes for sending email |
| app/jobs | Background jobs | ActiveJob classes for async processing |
| app/channels | WebSocket handlers | ActionCable channel definitions |
| app/assets | Static assets | Images, stylesheets, JavaScript |
| config | Configuration | Database, routes, environment settings |
| db | Database files | Migrations, schema, seeds |
| lib | Custom libraries | Reusable code not part of core app |
| public | Static files | Files served directly by web server |
| test or spec | Tests | All test files and fixtures |
| vendor | Third-party code | Vendored gems and assets |
RubyGems Structure
| Directory | Purpose | Required |
|---|---|---|
| lib | Gem source code | Yes |
| bin | Executable commands | No |
| test or spec | Test suite | Recommended |
| docs | Documentation | No |
| examples | Usage examples | No |
File Naming Conventions
| Constant | File Path | Pattern |
|---|---|---|
| PaymentGateway | lib/payment_gateway.rb | snake_case file name |
| PaymentGateway::Client | lib/payment_gateway/client.rb | Namespace mirrors directories |
| PaymentGateway::API::V1 | lib/payment_gateway/api/v1.rb | Multi-level namespace |
| Admin::UsersController | app/controllers/admin/users_controller.rb | Rails controller naming |
Directory Depth Guidelines
| Project Size | Recommended Depth | Rationale |
|---|---|---|
| Small (< 50 files) | 1-2 levels | Minimize navigation overhead |
| Medium (50-500 files) | 2-3 levels | Balance organization with navigation |
| Large (500+ files) | 3-4 levels | Support modular organization |
| Very Large (5000+ files) | 4-5 levels | Enable domain isolation |
Autoload Configuration Patterns
| Pattern | Configuration | Use Case |
|---|---|---|
| Custom lib directory | config.autoload_paths += lib | Autoload custom libraries |
| Service objects | config.autoload_paths += app/services | Load service layer |
| Multiple domains | config.autoload_paths += domains/* | Domain-driven structure |
| Engine integration | isolate_namespace in engine | Isolated engine namespaces |
Directory Structure Decision Matrix
| Requirement | Recommended Structure | Alternative |
|---|---|---|
| Small gem or library | Standard gem layout with lib, test, bin | Single-file gem |
| Rails web application | Standard Rails MVC structure | Feature-based slices |
| Microservices monorepo | Service directories with shared code | Polyrepo with separate gems |
| Component-based UI | Component directories with colocated assets | Separate asset directories |
| Domain-driven design | Domain directories with internal layers | Traditional layered structure |
| API-only application | Controllers and models without views | Grape or Sinatra structure |