Organizing Code with require

Published by Programming Line in Ruby. Last updated .

Ruby programs rarely live in a single file for long. As soon as you split logic into classes, modules, or reusable libraries across multiple files, you need a way to load those files into your running program — that’s the job of require. Ruby also gives you require_relative for loading your own project files by their location on disk, and load for re-running a file’s code from scratch. Understanding how these three work, and how Ruby keeps track of what’s already been loaded, is essential for organizing any Ruby project larger than a script.

Overview: How require Works

Ruby has no import keyword or per-file namespace system like some other languages. require simply finds a file on disk and executes its top-level Ruby code exactly once, as if you had typed that code directly into your program at the point of the require call. Whatever classes, modules, constants, or methods that file defines at the top level become globally visible afterward — there is no "export list" to opt into.

$LOAD_PATH (also readable as $:) is an array of directories Ruby searches when you call require with a bare name like require "json". Ruby’s standard library directories are on $LOAD_PATH automatically, and installed gems add their lib/ folders to it once activated (Bundler handles this for you in a real project). Since Ruby 1.9, the current directory is not on $LOAD_PATH by default, which is exactly why plain require is the wrong tool for your own project files.

Ruby remembers every file it has successfully required in a global array called $LOADED_FEATURES (aliased as $"). Before loading anything, require checks whether the file’s absolute path is already in that list. If it is, require does nothing and returns false. If it isn’t, Ruby loads and executes the file, adds its path to $LOADED_FEATURES, and returns true. This is what makes it safe to require the same file from ten different places in a codebase — it only ever runs once.

require_relative is require‘s sibling for your own code. Instead of searching $LOAD_PATH, it resolves the given path relative to the directory of the file that calls it, using the special __FILE__ constant internally. That means require_relative "greeter" always finds greeter.rb next to the file that requires it, no matter what directory you happen to run your program from. The convention is: use require for gems and the standard library, and require_relative for files that belong to your own project.

load is different again: it re-executes a file’s code every time you call it, completely ignoring $LOADED_FEATURES. It returns true or raises LoadError, never false. It’s mostly used for interactive reloading (re-running a script after you’ve edited it, without restarting your process) rather than for everyday code organization.

Syntax

require "library_name"        # search $LOAD_PATH; for gems & stdlib
require_relative "path"       # relative to the current file's directory
load "path.rb"                # re-executes every time, needs .rb extension
$LOAD_PATH.unshift(dir)        # add a directory to the search path
__dir__                        # absolute directory of the current file
__FILE__                       # absolute path of the current file
Form Resolves against Runs again if called twice? Typical use
require $LOAD_PATH No (returns false) Gems, standard library (json, set, date)
require_relative Calling file’s directory No (returns false) Your own project’s files
load $LOAD_PATH or a full path Yes, always Interactive reloading, one-off scripts

Examples

Example 1: Loading a standard library file

The standard library ships with Ruby but still has to be explicitly loaded. Here, require "json" makes the JSON module available.

require "json"

data = { name: "Ruby", year: 1995, mutable: true }
json_text = JSON.generate(data)
puts json_text

parsed = JSON.parse(json_text)
puts parsed["name"]
puts parsed.class

Output:

{"name":"Ruby","year":1995,"mutable":true}
Ruby
Hash

Before this require, calling JSON.generate would raise NameError: uninitialized constant JSON — the module simply doesn’t exist in your program until its file has been loaded. Note also that JSON.parse returns a plain Hash with string keys by default, not the symbol keys you passed in.

Example 2: Splitting code across files with require_relative

This example writes a small library file to disk next to the script and then loads it with require_relative, exactly the pattern you’d use with real project files.

lib_path = File.join(__dir__, "greeter.rb")
File.write(lib_path, <<~RUBY)
  class Greeter
    def initialize(name)
      @name = name
    end

    def greet
      "Hello, #{@name}! Welcome to Ruby."
    end
  end
RUBY

require_relative "greeter"

greeter = Greeter.new("Alex")
puts greeter.greet
puts defined?(Greeter)

Output:

Hello, Alex! Welcome to Ruby.
constant

require_relative "greeter" resolves to greeter.rb in the same directory as the running script, regardless of the shell’s current working directory. Once loaded, Greeter is a normal global constant — defined?(Greeter) confirms it exists and reports its kind as "constant".

Example 3: require only loads a file once

first_load = require "json"
second_load = require "json"

puts first_load
puts second_load
puts $LOADED_FEATURES.any? { |path| path.end_with?("json.rb") }

Output:

true
false
true

The first require "json" actually loads the library and returns true. The second call finds json.rb‘s path already sitting in $LOADED_FEATURES, skips loading it again, and returns false. This return value is genuinely useful — some libraries use require "foo" and do_something to run setup code only the first time a file is loaded.

How require Works Step by Step

When Ruby evaluates a require (or require_relative) call, roughly this happens:

  • Ruby resolves the argument to an absolute file path — searching $LOAD_PATH for require, or resolving relative to __FILE__ for require_relative.
  • It checks whether that absolute path is already recorded in $LOADED_FEATURES. If so, it stops here and returns false.
  • If not, Ruby immediately adds the path to $LOADED_FEATURES (before running the file’s code — this is what prevents infinite loops in circular requires).
  • Ruby then reads and executes the file’s top-level Ruby code from top to bottom, exactly as if it were pasted in at the call site. Any class, module, constant, or top-level method definitions take effect globally.
  • If the file raises an error partway through, the require call propagates that error, and the path is removed from $LOADED_FEATURES so a later require can retry.
  • On success, require returns true. If the file can’t be found at all, Ruby raises LoadError.

Common Mistakes

Mistake 1: Using require for your own project files

Plain require searches $LOAD_PATH, which does not include your project’s own folders. A relative-looking path passed to require often works by accident while you’re in a particular directory, then breaks the moment someone runs the script from elsewhere.

# main.rb (sits in the same folder as greeter.rb)
require "greeter"

puts Greeter.new("Sam").greet

Output:

main.rb:1:in 'require': cannot load such file -- greeter (LoadError)

The fix is to use require_relative, which always resolves against main.rb‘s own directory regardless of the current working directory:

require_relative "greeter"

puts Greeter.new("Sam").greet

Output:

Hello, Sam! Welcome to Ruby.

Mistake 2: Circular require_relative between two files

When file A requires file B, and file B also requires file A, Ruby’s loop guard prevents infinite recursion — but it does so by marking a file as "loaded" the moment it starts, not when it finishes. If one file references a constant from the other at the top level (outside a method body), that constant may not exist yet.

# a.rb
require_relative "b"

class A
  GREETING = "hi from A"
end

puts "In a.rb, B::MESSAGE = #{B::MESSAGE}"

# b.rb
require_relative "a"

class B
  MESSAGE = "hi from B, and A::GREETING = #{A::GREETING}"
end

Output (running b.rb):

b.rb:4:in '<class:B>': uninitialized constant A (NameError)

Running b.rb triggers require_relative "a", which triggers require_relative "b" again — Ruby sees b.rb is already being loaded and skips it silently, so a.rb continues and finishes defining A. But by the time control returns to finish loading b.rb, the order in which the constant is actually needed can easily flip depending on which file is the entry point, and you get an intermittent NameError. The real fix is to remove the circular dependency: pull the shared piece into its own file that both sides require independently.

lib_dir = __dir__

File.write(File.join(lib_dir, "a2.rb"), <<~RUBY)
  class A2
    VERSION = "1.0"
  end
RUBY

File.write(File.join(lib_dir, "b2.rb"), <<~RUBY)
  class B2
    def report
      "A2::VERSION is #{A2::VERSION}"
    end
  end
RUBY

require_relative "a2"
require_relative "b2"

puts B2.new.report

Output:

A2::VERSION is 1.0

b2.rb only references A2::VERSION inside a method body, so it’s not evaluated until report is actually called — by which point both files have finished loading in a predictable order.

Mistake 3: Expecting require to pick up edited code

Because require checks $LOADED_FEATURES first, calling it again after editing a file does nothing — the file is already marked as loaded, so your changes are silently ignored.

irb> require "./greeter"
=> true
irb> # ...edit greeter.rb to change the greeting message...
irb> require "./greeter"
=> false
irb> Greeter.new("Sam").greet
=> "Hello, Sam!"   # still the OLD code -- require skipped the file
irb> load "./greeter.rb"
=> true
irb> Greeter.new("Sam").greet
=> "Hi there, Sam!"   # now it picks up the edited code

Use load with an explicit path when you deliberately want a file re-executed, such as reloading code in a long-running console session.

Best Practices

  • Use require only for gems and the standard library; use require_relative for anything inside your own project.
  • Put all require/require_relative calls at the very top of a file so a reader can see its dependencies at a glance.
  • Name files after the constant they define — greeter.rb defines Greeter — so require_relative calls read predictably.
  • Avoid mutual require_relative calls between two files; if two classes genuinely need each other, extract the shared piece into a third file that both require.
  • Reach for load only when you deliberately want a file’s top-level code to re-run; it’s rarely the right tool in application code.
  • In a real application, let Bundler (Bundler.require) or a single loader file manage gem requires in the correct order rather than requiring every gem by hand in every file.
  • Remember there’s no per-file namespace: everything a required file defines at the top level lands in the same global constant table, so wrap your code in modules or classes to avoid naming collisions.

Practice Exercises

1. Create two files, animal.rb (a class Animal with a speak method) and main.rb that loads it with require_relative and calls speak. Confirm it still works when you run it from a different working directory.

2. Take a script that defines three unrelated classes in one file and split it into three separate files plus one entry file that requires all three. The program’s output should be identical before and after the split.

3. Deliberately create two files that require_relative each other and reference one another’s constants at the top level (not inside a method). Run it, observe the NameError, then fix it by extracting the shared constant into its own file.

Summary

  • require loads a file by searching $LOAD_PATH and is meant for gems and the standard library.
  • require_relative loads a file relative to the calling file’s own directory and is the right choice for your own project code.
  • Ruby tracks every loaded file’s absolute path in $LOADED_FEATURES; a repeated require of the same file is a no-op that returns false.
  • load re-executes a file’s code every time it’s called and ignores $LOADED_FEATURES entirely.
  • Required files have no private namespace — everything they define at the top level becomes a global constant.
  • Circular require_relative calls that reference each other’s constants outside a method body are fragile and order-dependent; break the cycle by extracting shared code into its own file.