Modules Explained

Published by Programming Line in Ruby. Last updated .

A module in Ruby is a container for methods, classes, and constants that — unlike a class — can never be instantiated with .new and can never be subclassed. That single restriction is what makes modules so useful: Ruby leans on them for two distinct jobs, organizing code into namespaces so names don’t collide, and mixing shared behavior into otherwise unrelated classes without resorting to multiple inheritance. Once you see how include, extend, and prepend actually change an object’s method lookup chain, modules stop feeling like a separate topic and start feeling like the same object model you already know, applied more flexibly.

Overview / How Modules Work

A module is defined with module…end, and it looks a lot like a class. The difference is behavioral: you can’t write SomeModule.new, and a module can’t appear as a class’s superclass. Because a module can’t sit in a normal inheritance chain the way a class does, Ruby uses it for two very different jobs instead: namespacing (grouping related classes, methods, and constants under one name) and mixins (packaging a set of methods that can be dropped into any class to give it shared behavior).

Ruby deliberately does not support multiple inheritance — a class can only have one superclass. Modules are the sanctioned workaround. When you write include SomeModule inside a class body, Ruby doesn’t copy the module’s methods into the class. Instead, it splices the module into the class’s ancestor chain, immediately above the class itself. Every object has an ancestor chain, and every method call walks that chain from most specific to least specific, looking for a matching method: first the object’s singleton class (if it has singleton methods), then any modules prepended to the class, then the class itself, then any included modules (searched in the reverse order they were included), then the superclass and its own included modules, and so on up through Object, Kernel, and finally BasicObject. You can inspect this chain yourself by calling SomeClass.ancestors.

This is also the mechanism behind super: calling super doesn’t mean “look in the parent class” so much as “continue the same lookup along the ancestor chain from just after where the current method was found.” That’s why a module method and a class’s own method of the same name can cooperate through super without either one needing to know the other exists.

There’s a close relative of include called extend. Where include mixes a module’s methods in as instance methods, extend mixes them in as methods on one specific object — most commonly used inside a class body to add class-level (“static-like”) methods. And prepend, added later to the language, inserts the module before the class in the ancestor chain instead of after, letting the module’s method run first and optionally call super to reach the class’s own implementation — a clean way to wrap or decorate existing behavior.

Syntax

The most basic module, used as a namespace with a module-level (“static”) method:

module Greeter
  def self.hello(name)
    "Hi, #{name}!"
  end
end

puts Greeter.hello("Sam")

Output:

Hi, Sam!
Keyword What it does Typical use
include Adds the module’s methods as instance methods, inserted just above the class in the ancestor chain Sharing behavior between objects, e.g. Comparable, Enumerable
extend Adds the module’s methods directly to the receiver’s singleton class Adding class-level methods, or extra behavior on one object
prepend Inserts the module below the class in the ancestor chain, so its methods run before the class’s own Wrapping or decorating an existing method, then calling super
module_function Turns subsequent methods into private instance methods and public methods on the module itself Utility modules, similar to Math
Namespace::Const Accesses a constant or class nested inside a module Avoiding naming collisions

Examples

Example 1: Using a Module as a Namespace

The simplest use of a module has nothing to do with mixins — it’s just a named box that keeps related classes and constants together and out of the global namespace:

module Shapes
  PI = 3.14159

  class Circle
    def initialize(radius)
      @radius = radius
    end

    def area
      (Shapes::PI * @radius ** 2).round(2)
    end
  end
end

circle = Shapes::Circle.new(5)
puts circle.area
puts Shapes::PI

Output:

78.54
3.14159

Because Circle is defined inside Shapes, its full name is Shapes::Circle, and it reaches the module’s constant with Shapes::PI (or just PI from inside the module body, since constant lookup checks the lexically enclosing scope first). If another part of the program also defined a class called Circle, the two would never collide, because they live in different namespaces.

Example 2: Sharing Behavior with a Mixin

The more common use of modules is packaging behavior that multiple, otherwise unrelated classes can share by calling include:

module Greetable
  def greet
    "Hello, my name is #{name}!"
  end
end

class Person
  include Greetable

  attr_reader :name

  def initialize(name)
    @name = name
  end
end

alice = Person.new("Alice")
puts alice.greet
puts Person.ancestors.inspect

Output:

Hello, my name is Alice!
[Person, Greetable, Object, Kernel, BasicObject]

Greetable doesn’t know anything about Person specifically — it just assumes whatever class includes it defines a name method. Any class that provides one, from a Product to a Pet, could include Greetable and get the same greet method for free. That’s the essence of a mixin: behavior defined once, usable across classes that share nothing else.

Example 3: Mixing In Comparable

Ruby’s standard library ships several ready-made mixins, and Comparable is one of the most useful. Define a single method, <=> (the “spaceship operator”, returning -1, 0, or 1), include Comparable, and you get <, >, <=, >=, ==, between?, clamp, and the ability to sort a collection of your objects, all for free:

class Money
  include Comparable

  attr_reader :cents

  def initialize(cents)
    @cents = cents
  end

  def <=>(other)
    cents <=> other.cents
  end

  def to_s
    "$#{cents / 100.0}"
  end
end

prices = [Money.new(500), Money.new(150), Money.new(1000)]
puts prices.sort.map(&:to_s).join(", ")
puts Money.new(300) > Money.new(200)

Output:

$1.5, $5.0, $10.0
true

sort works because Array#sort compares elements with <=>, and Comparable is what turns that one method into every other comparison operator. Write <=> once, get more than half a dozen methods back.

How It Works Step by Step

Walking through alice.greet from Example 2 shows exactly what include did:

  1. alice.greet is called. Ruby first checks alice‘s singleton class — no singleton method named greet is defined there, so the search continues.
  2. Next Ruby checks the Person class itself. Person doesn’t define greet directly.
  3. Next it checks modules included into Person, and finds Greetable, which does define greet. The search stops here and that method runs.
  4. Inside greet, self is still alice — mixin methods run with the including object as self, exactly like a method defined directly on the class — so name resolves through alice‘s own attr_reader, returning "Alice".
  5. Person.ancestors prints the exact chain that was searched: [Person, Greetable, Object, Kernel, BasicObject]. Greetable sits directly above Person because that’s where include spliced it in.

Common Mistakes

Mistake 1: calling an instance method on the module itself. A method defined normally inside a module (def double(x)) is an instance method — it only becomes available once the module is mixed into a class with include. Calling it directly on the module, as if the module understood it, raises NoMethodError:

module MathHelpers
  def double(x)
    x * 2
  end
end

puts MathHelpers.double(4)

To call a method directly on the module — the way you’d call Math.sqrt(9) — either define it with def self.method_name, or add module_function above the definitions so each one becomes both a private instance method and a public method on the module:

module MathHelpers
  module_function

  def double(x)
    x * 2
  end
end

puts MathHelpers.double(4)

Output:

8

Mistake 2: assuming a constant is fully immutable. A constant only locks the reference — reassigning it elsewhere triggers a warning, but nothing stops code from mutating the array or hash the constant points to. Passing a constant into a method that mutates it with << silently changes it everywhere it’s used:

module Config
  DEFAULT_TAGS = ["ruby", "web"]
end

def add_tag(tags)
  tags << "backend"
end

add_tag(Config::DEFAULT_TAGS)
puts Config::DEFAULT_TAGS.inspect

Output:

["ruby", "web", "backend"]

Freeze the array when you define it, and have callers build a new collection instead of mutating the shared one:

module Config
  DEFAULT_TAGS = ["ruby", "web"].freeze
end

def add_tag(tags)
  tags + ["backend"]
end

new_tags = add_tag(Config::DEFAULT_TAGS)
puts new_tags.inspect
puts Config::DEFAULT_TAGS.inspect

Output:

["ruby", "web", "backend"]
["ruby", "web"]

Best Practices

  • Prefer mixing in a small, focused module over building a deep class hierarchy just to share a couple of methods.
  • Name mixins that add a capability with an -able adjective (Comparable, Enumerable, Greetable) so their purpose is obvious at the include site.
  • For a module that’s purely a bag of utility methods, never meant to be mixed in, make every method a module method with module_function or def self.method_name rather than leaving them as unreachable instance methods.
  • Document what method(s) a mixin expects the including class to define — Comparable expects <=>; your own module should be just as explicit.
  • Freeze constants that hold mutable objects like arrays and hashes so a stray << elsewhere in the codebase can’t silently mutate shared state.
  • Reach for prepend only when you specifically need to run code before or around an existing method and call super; for ordinary behavior sharing, include is simpler and far more common.

Practice Exercises

1. Write a module called Loggable with one instance method, log(message), that prints [ClassName] message using the including object’s own class name (hint: self.class.name inside the module method). Include it in two unrelated classes, such as Order and User, and confirm each one logs with its own class name.

2. Build a namespace module named Bank containing a class Account and a module-level constant MIN_BALANCE. Access the constant from outside the module with Bank::MIN_BALANCE, and create an account with Bank::Account.new.

3. Create a module Sizeable that includes Comparable and defines <=> in terms of a size method that any including class must provide. Mix it into two different classes (say Box and Crate), give each a size, and confirm you can sort an array containing instances of both by size.

Summary

  • A module is defined with module…end; it can never be instantiated or subclassed.
  • include inserts a module’s instance methods into a class’s ancestor chain, just above the class.
  • extend adds a module’s methods as methods on the receiver itself, commonly used for class-level methods.
  • prepend inserts a module below the class in the chain, letting it intercept calls and forward to the original with super.
  • Method lookup always walks a fixed order: singleton class, prepended modules, the class, included modules (most recently included first), then superclasses up to BasicObject — check it anytime with SomeClass.ancestors.
  • Modules can also namespace classes and constants (Namespace::Thing) to avoid name collisions, entirely independent of mixins.
  • module_function or def self.method_name turns a module’s methods into callable module-level utilities, similar to Math.sqrt.
  • Constants only protect the reference from reassignment — the object itself stays mutable unless you freeze it.