CrackedRuby CrackedRuby

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