Ruby

Ruby Modules and Mixins: include, extend, prepend, and Rails Concerns

Ruby Modules and Mixins: include, extend, prepend, and Rails Concerns

A Ruby module is a container for methods and constants that can’t be instantiated. As a namespace it prevents name clashes; as a mixin it adds behavior to a class: include adds instance methods, extend adds class methods, and prepend inserts the module before the class so its methods run first. Rails’ ActiveSupport::Concern builds on the same mechanism.

That is the short version. The rest covers namespaces and constant lookup, the three mixin verbs with the ancestors chain that explains them, and mixin hooks. It also covers the standard library’s Comparable and Enumerable, refinements, ActiveSupport::Concern on Rails 8.1, and the seven module errors developers paste into a search box. Every snippet and every error message here ran on Ruby 3.4.10 and Rails 8.1.3.1.

What Are Modules in Ruby?

A module starts with the module keyword instead of class and, like a class, groups methods and constants under one name. The two are close relatives: Class is a subclass of Module, so every class is a module with the extra ability to create objects.

Ruby
Class.is_a?(Module) # => true
Class.superclass # => Module

What a plain module lacks is exactly what makes it useful as a mixin:

  • It can’t be instantiated: InvoiceCreator.new raises undefined method 'new' for module InvoiceCreator (NoMethodError).
  • It can’t be inherited from: class Invoice < InvoiceCreator raises superclass must be an instance of Class (given an instance of Module) (TypeError).
  • It gets mixed into classes instead, with include, extend, or prepend.

Modules are not flat, though. A module can include other modules and has an ancestors list of its own, which matters once several mixins stack up.

Here is a module that holds a constant and both kinds of method a module can provide:

Ruby
module InvoiceCreator
  TAX_FEE = 0.5
 
  def self.generate
    puts "Don't worry! I'll generate the invoice for you at #{TAX_FEE}%"
  end
 
  def invoice_total
    puts "I'll return the invoice total"
    1000
  end
end

self.generate is a module method. It lives on the module object itself, so you call it without mixing anything in, which is the usual shape of a small service module:

Ruby
InvoiceCreator.generate
Shell
$ ruby invoice_creator.rb
Don't worry! I'll generate the invoice for you at 0.5%

invoice_total is an instance method, and a module has no instances. To call it, a class has to include the module:

Ruby
class Invoice
  include InvoiceCreator
 
  def calculate_tax
    total = invoice_total # included method from our module
    tax = total * InvoiceCreator::TAX_FEE
    puts "This is the invoice tax: #{tax}"
  end
end
 
Invoice.new.calculate_tax
Shell
$ ruby invoice.rb
I'll return the invoice total
This is the invoice tax: 500.0

Every instance method of InvoiceCreator is now an instance method of Invoice. The constant travels too: Invoice::TAX_FEE returns 0.5, and inside calculate_tax a bare TAX_FEE would have resolved as well; constants through mixins explains why.

Utility modules often want a method available both ways: as TextUtil.slugify from anywhere, and as a bare slugify inside a class that includes TextUtil. module_function does that in one line. Every method defined after it becomes a module method and a private instance method:

Ruby
module TextUtil
  module_function
 
  def slugify(text)
    text.downcase.strip.gsub(/[^a-z0-9]+/, "-").delete_suffix("-")
  end
end
 
class Post
  include TextUtil
 
  def slug
    slugify("Mixins and Modules!")
  end
end
 
TextUtil.slugify("Mixins and Modules!") # => "mixins-and-modules"
Post.new.slug # => "mixins-and-modules"
Post.new.slugify("x")
# => private method 'slugify' called for an instance of Post (NoMethodError)

The private copy keeps slugify out of Post’s public interface, so it never shows up on an object that has nothing to do with text.

Modules as Namespaces

The second thing a module does is hold a name. Suppose the application needs two invoice creators, one for customers and one for suppliers. Two top-level InvoiceCreator modules would be one module: Ruby reopens a module that already exists, and the second def self.generate replaces the first without a word. Nesting each one inside a module of its own gives each a separate name:

Ruby
module Customer
  module InvoiceCreator
    def self.generate
      puts "Don't worry! I'll generate the customer invoice for you"
    end
  end
end
 
module Supplier
  module InvoiceCreator
    def self.generate
      puts "Don't worry! I'll generate the supplier invoice for you"
    end
  end
end
 
Customer::InvoiceCreator.generate
Supplier::InvoiceCreator.generate
Shell
$ ruby namespaces.rb
Don't worry! I'll generate the customer invoice for you
Don't worry! I'll generate the supplier invoice for you

:: walks into a namespace from the outside. From the inside, Ruby resolves an unqualified constant through Module.nesting first: the modules and classes lexically enclosing the current line, innermost first. Then it checks the ancestors of the innermost one. The nesting depends on how the definition was written, not on the resulting name:

Ruby
module Billing
  TAX = 0.2
 
  class Receipt
    def nesting
      Module.nesting
    end
 
    def tax
      TAX
    end
  end
end
 
class Billing::Invoice
  def nesting
    Module.nesting
  end
 
  def tax
    TAX
  end
end
 
Billing::Receipt.new.nesting # => [Billing::Receipt, Billing]
Billing::Receipt.new.tax # => 0.2
Billing::Invoice.new.nesting # => [Billing::Invoice]
Billing::Invoice.new.tax
# => uninitialized constant Billing::Invoice::TAX (NameError)

The compact class Billing::Invoice form skips Billing in the nesting, so a bare TAX has nowhere to resolve. Nest the definitions, or write Billing::TAX; the NameError entry has the full message.

Modules as Mixins: include, extend, and prepend

Ruby has single inheritance: a class has exactly one superclass. Behavior that several unrelated classes share can’t come from a common parent, so it comes from modules mixed in, which is composition over inheritance as the language ships it. Three verbs do the mixing, and the way to tell them apart is ancestors: the ordered list of classes and modules Ruby searches when you call a method. One module serves all three examples:

Ruby
module Greeting
  def greet
    "hello from #{self.class}"
  end
end

include: Instance Methods and the Ancestors Chain

include inserts the module into the ancestors chain directly after the class. Instances gain the module’s instance methods, and the module shows up in type checks:

Ruby
class Person
  include Greeting
end
 
Person.ancestors # => [Person, Greeting, Object, Kernel, BasicObject]
Person.new.greet # => "hello from Person"
Person.include?(Greeting) # => true
Person.new.is_a?(Greeting) # => true

When greet is called on a Person, Ruby walks that list from the left: Person has no greet, Greeting does. Kernel near the end is a module too, included in Object; puts and require come from there.

extend: Methods on the Object Itself

extend adds a module’s methods to one object rather than to a class’s instances. It is include applied to that object’s singleton class, which is why the class’s own ancestors don’t change:

Ruby
class Company
  extend Greeting
end
 
Company.ancestors # => [Company, Object, Kernel, BasicObject]
Company.singleton_class.ancestors # => [#<Class:Company>, Greeting, #<Class:Object>, #<Class:BasicObject>, Class, Module, Object, Kernel, BasicObject]
Company.greet # => "hello from Class"

Company.greet reports hello from Class because inside the method self is the Company class object, whose class is Class. Extending a class is how a module provides class methods. Extending any other object gives that single object the methods and leaves its siblings alone:

Ruby
report = Object.new
report.extend(Greeting)
report.greet # => "hello from Object"
Object.new.respond_to?(:greet) # => false

prepend: Running Before the Class

prepend places the module before the class in ancestors. Ruby finds the module’s method first, even when the class defines a method with the same name:

Ruby
class Robot
  prepend Greeting
 
  def greet
    "beep"
  end
end
 
Robot.ancestors # => [Greeting, Robot, Object, Kernel, BasicObject]
Robot.new.greet # => "hello from Robot"

Robot#greet never runs. The useful version of this is a module that calls super, which wraps the original method instead of replacing it:

Ruby
module Loud
  def greet
    super.upcase + "!"
  end
end
 
class Shouter
  prepend Loud
 
  def greet
    "hi"
  end
end
 
Shouter.ancestors # => [Loud, Shouter, Object, Kernel, BasicObject]
Shouter.new.greet # => "HI!"

Because Shouter#greet still exists and super reaches it, this is the safe way to change a method you don’t own, and the standard alternative to reopening a class. Responsible Monkeypatching in Ruby weighs prepend against the other options for third-party code.

Method Lookup, super, and Several Mixins

Every lookup follows one rule: walk ancestors left to right and stop at the first match. That rule settles what happens when two included modules define the same method. Each include inserts its module directly after the class, so the module included last sits closest to the class and wins. The earlier one is still there, one step further along, and super reaches it:

Ruby
module First
  def name
    "first"
  end
end
 
module Second
  def name
    "second, then #{super}"
  end
end
 
class Both
  include First
  include Second
end
 
Both.ancestors # => [Both, Second, First, Object, Kernel, BasicObject]
Both.new.name # => "second, then first"

The methods don’t overwrite each other; they shadow each other, and super peels the layers back. A method the class defines itself beats every included module, and can still fall through to them:

Ruby
module Fallback
  def greet
    "fallback"
  end
end
 
class Overrider
  include Fallback
 
  def greet
    "class first, then #{super}"
  end
end
 
Overrider.new.greet # => "class first, then fallback"

Including a module that is already in the chain changes nothing:

Ruby
Person.include(Greeting)
Person.ancestors # => [Person, Greeting, Object, Kernel, BasicObject]

Modules stack the same way. A module can include other modules, and a class that includes the outer module gets the whole chain:

Ruby
module InvoiceCalculator
  def calculate_items_total
    puts "imagine some math here"
  end
end
 
module InvoiceRenderer
  def generate_invoice_pdf
    puts "imagine that we are using some PDF generation magic here"
  end
end
 
module InvoiceSender
  def send_invoice
    puts "imagine your favorite mail service being used here"
  end
end
 
module Customer
  module InvoiceCreator
    include InvoiceCalculator
    include InvoiceRenderer
    include InvoiceSender
 
    def generate_invoice
      calculate_items_total # from InvoiceCalculator
      generate_invoice_pdf # from InvoiceRenderer
      send_invoice # from InvoiceSender
    end
  end
end
 
class Invoice
  include Customer::InvoiceCreator
end
 
Invoice.new.generate_invoice
Shell
$ ruby invoice_chain.rb
imagine some math here
imagine that we are using some PDF generation magic here
imagine your favorite mail service being used here

Invoice includes one module and ends up with four in its chain, in the order the lookup rule predicts:

Ruby
Customer::InvoiceCreator.ancestors # => [Customer::InvoiceCreator, InvoiceSender, InvoiceRenderer, InvoiceCalculator]
Invoice.ancestors # => [Invoice, Customer::InvoiceCreator, InvoiceSender, InvoiceRenderer, InvoiceCalculator, Object, Kernel, BasicObject]

Composition by mixin is one option. When the shared behavior belongs to a separate object, forwarding calls to that object is the other, and How to Delegate Methods in Ruby covers it.

Constants Through Mixins

Constants ride along with include. A constant defined in a module is reachable through the including class, from outside with :: and from inside with a bare name. Constant lookup walks the ancestors after it has checked the lexical nesting:

Ruby
module Limits
  MAX_ITEMS = 10
end
 
class Cart
  include Limits
 
  def full?(count)
    count >= MAX_ITEMS
  end
end
 
Cart::MAX_ITEMS # => 10
Cart.new.full?(10) # => true
Cart.constants # => [:MAX_ITEMS]
Cart.constants(false) # => []

constants(false) shows the difference: Cart can see MAX_ITEMS, but doesn’t own it.

Mixin Hooks: included, extended, and prepended

Ruby calls a hook on the module each time it is mixed in: self.included(base), self.extended(base), and self.prepended(base), where base is the class or object receiving the module. The classic use is providing class methods alongside instance methods. The module supplies instance methods as usual and, in included, extends the host with a nested ClassMethods module:

Ruby
module Trackable
  def self.included(base)
    puts "Trackable included in #{base}"
    base.extend(ClassMethods)
  end
 
  module ClassMethods
    def tracked?
      true
    end
  end
 
  def track
    "tracking #{self.class}"
  end
end
 
class Widget
  include Trackable
end
 
p Widget.tracked?
p Widget.new.track
Shell
$ ruby trackable.rb
Trackable included in Widget
true
"tracking Widget"

One include line gives Widget an instance method and a class method. An included hook is also where a mixin can generate methods on the host class; define_method is the tool, and our metaprogramming guide shows how it builds methods from a list of names. Rails packages the hook-plus-ClassMethods pattern as ActiveSupport::Concern.

Standard Library Mixins: Comparable and Enumerable

Ruby’s own library relies on the same mechanism: implement one method, include a module, receive a whole interface.

Comparable asks for <=>, which returns -1, 0, 1, or nil when the two objects can’t be compared. In return it provides exactly seven methods: <, <=, ==, >, >=, between?, and clamp:

Ruby
class Version
  include Comparable
  attr_reader :major, :minor
 
  def initialize(string)
    @major, @minor = string.split(".").map(&:to_i)
  end
 
  def <=>(other)
    return nil unless other.is_a?(Version)
 
    [major, minor] <=> [other.major, other.minor]
  end
 
  def to_s
    "#{major}.#{minor}"
  end
end
 
Comparable.instance_methods(false).sort # => [:<, :<=, :==, :>, :>=, :between?, :clamp]
Version.new("3.4") > Version.new("3.3") # => true
Version.new("3.2").between?(Version.new("3.0"), Version.new("3.4")) # => true
Version.new("4.0").clamp(Version.new("3.0"), Version.new("3.4")).to_s # => "3.4"
[Version.new("3.4"), Version.new("3.0"), Version.new("3.2")].sort.map(&:to_s) # => ["3.0", "3.2", "3.4"]
[Version.new("3.4"), Version.new("3.0")].max.to_s # => "3.4"
Version.new("3.4") < "3.3"
# => comparison of Version with String failed (ArgumentError)

The nil return is what produces that last ArgumentError instead of a confusing NoMethodError from inside <=>. Leave <=> out entirely and every comparison fails the same way, against the class itself:

Ruby
class Version
  include Comparable
 
  def initialize(string)
    @parts = string.split(".").map(&:to_i)
  end
end
 
Version.new("3.4") < Version.new("3.3")
Shell
$ ruby version_missing.rb
version_missing.rb:9:in 'Comparable#<': comparison of Version with Version failed (ArgumentError)

Version.new("3.4") < Version.new("3.3")
                     ^^^^^^^^^^^^^^^^^^
	from version_missing.rb:9:in '<main>'

Enumerable asks for each and gives back 61 methods on Ruby 3.4, map, select, sort_by, each_slice, and lazy among them:

Ruby
class Playlist
  include Enumerable
 
  def initialize(*tracks)
    @tracks = tracks
  end
 
  def each(&block)
    @tracks.each(&block)
  end
end
 
playlist = Playlist.new("Intro", "Verse", "Chorus")
playlist.map(&:upcase) # => ["INTRO", "VERSE", "CHORUS"]
playlist.select { |t| t.size > 5 } # => ["Chorus"]
playlist.sort_by(&:size) # => ["Intro", "Verse", "Chorus"]
playlist.include?("Verse") # => true
playlist.each_slice(2).to_a # => [["Intro", "Verse"], ["Chorus"]]
playlist.lazy.map(&:upcase).first(2) # => ["INTRO", "VERSE"]
Enumerable.instance_methods(false).size # => 61
Playlist.instance_method(:map).owner # => Enumerable

Forget each and the failure surfaces inside the module: Enumerable#map calls each on your object and raises undefined method 'each' for an instance of Playlist (NoMethodError). Our guide to Enumerable and Enumerator goes through what the module builds on top of each.

Refinements: Mixins with a Scope

include and prepend change a class for the whole program. A refinement changes it for one lexical scope. You define it with refine inside a module and switch it on with using; before that line, and in every other file, "hi".shout raises undefined method 'shout' for an instance of String (NoMethodError):

Ruby
module StringShout
  refine String do
    def shout
      upcase + "!"
    end
  end
end
 
StringShout.refinements # => [#<refinement:String@StringShout>]
StringShout.refinements.first.target # => String
 
using StringShout
 
"hi".shout # => "HI!"

Refinements are the tool when a change must not leak, for example a helper on String that only one gem or one directory should see. The monkeypatching post weighs them against prepend for changes that should apply everywhere.

ActiveSupport::Concern in Rails

A Rails concern is a module that extends ActiveSupport::Concern. The extension adds three things to what a plain module already does: an included block that runs with the including class as self, a class_methods block that replaces the hand-written ClassMethods module, and dependency resolution between concerns. Every example in this section ran inside a fresh rails new app on Rails 8.1.3.1 with activesupport 8.1.3.1 and zeitwerk 2.8.3.

included do and class_methods do

A concern for models that need a URL slug:

Ruby
# app/models/concerns/sluggable.rb
module Sluggable
  extend ActiveSupport::Concern
 
  included do
    before_validation :set_slug
    validates :slug, presence: true
  end
 
  class_methods do
    def find_by_slug!(slug)
      find_by!(slug: slug)
    end
  end
 
  def set_slug
    self.slug ||= title.to_s.parameterize
  end
end
Ruby
# app/models/article.rb
class Article < ApplicationRecord
  include Sluggable
end

before_validation and validates are class-level calls, so they can’t sit in the module body, where self is Sluggable. Inside included do, self is Article, and the callback and the validator land on the model. class_methods do collects class methods into Sluggable::ClassMethods and extends the including class with it:

Ruby
Sluggable::ClassMethods.instance_methods(false) # => [:find_by_slug!]
Article.validators.map { |v| [v.class.name, v.attributes] } # => [["ActiveRecord::Validations::PresenceValidator", [:slug]]]
Article.new.valid? # => false
article = Article.create!(title: "Mixins and Modules")
article.slug # => "mixins-and-modules"
Article.find_by_slug!("mixins-and-modules") == article # => true

A prepended do block exists as well, for concerns that a class mixes in with prepend.

Concerns That Depend on Other Concerns

A concern can include another concern, and the outer one does not need to know what the inner one sets up:

Ruby
# app/models/concerns/publishable.rb
module Publishable
  extend ActiveSupport::Concern
 
  include Sluggable
 
  included do
    scope :published, -> { where.not(published_at: nil) }
  end
 
  class_methods do
    def publish_all!
      find_each(&:publish!)
    end
  end
 
  def publish!
    update!(published_at: Time.current)
  end
 
  def published?
    published_at.present?
  end
end
Ruby
# app/models/article.rb
class Article < ApplicationRecord
  include Publishable
end

ActiveSupport::Concern sees that Publishable is itself a concern and holds back Sluggable’s included block until a real class includes Publishable. Both blocks then run against Article, and both class_methods blocks extend it:

Ruby
Article.ancestors.first(4).map(&:name) # => ["Article", "Publishable", "Sluggable", "Article::GeneratedAssociationMethods"]
Article.respond_to?(:find_by_slug!) # => true
Article.respond_to?(:publish_all!) # => true
Publishable.respond_to?(:find_by_slug!) # => false

The same two modules written without ActiveSupport::Concern, using self.included(base) and base.before_validation, fail at load time with undefined method 'before_validation' for module PlainPublishable (NoMethodError): the hook ran against the intermediate module, which has no callbacks. That failure is the reason the concern module exists; the error entry has the code.

Where Concerns Live: app/models/concerns and Zeitwerk

Rails autoloads with Zeitwerk, and the concern directories are autoload roots, not namespaces. The first eight roots on a fresh 8.1 app:

Ruby
Rails.autoloaders.main.dirs.map { |dir| dir.delete_prefix("#{Rails.root}/") }.first(8)
# => ["lib", "app/controllers", "app/controllers/concerns", "app/helpers", "app/jobs", "app/mailers", "app/models", "app/models/concerns"]

So app/models/concerns/taggable.rb must define Taggable, and app/controllers/concerns/authenticatable.rb must define Authenticatable. The pre-Zeitwerk habit of writing Concerns::Taggable raises uninitialized constant Concerns (NameError), and a file that defines module Concerns::Taggable fails the same way when Zeitwerk loads it. Controller concerns work like model concerns:

Ruby
# app/controllers/concerns/authenticatable.rb
module Authenticatable
  extend ActiveSupport::Concern
 
  included do
    before_action :require_login
  end
 
  private
 
  def require_login
    head :unauthorized unless session[:user_id]
  end
end
Ruby
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  include Authenticatable
end
Ruby
ApplicationController.ancestors.first(2) # => [ApplicationController, Authenticatable]
ApplicationController._process_action_callbacks.map { |callback| [callback.kind, callback.filter] }.last # => [:before, :require_login]

The 2021 version of this post kept its module in lib/modules/invoice_creator.rb. On Rails 8.1 that no longer loads. The generated config/application.rb contains config.autoload_lib(ignore: %w[assets tasks]), which makes lib an autoload root, so Zeitwerk expects that file to define Modules::InvoiceCreator. A model with include InvoiceCreator raises uninitialized constant Invoice::InvoiceCreator (NameError), and bin/rails zeitwerk:check names the real problem:

Shell
$ bin/rails zeitwerk:check
Hold on, I am eager loading the application.
expected file lib/modules/invoice_creator.rb to define constant Modules::InvoiceCreator, but didn't

Move the file to lib/invoice_creator.rb, or into app/models/concerns/ if it is a model mixin, and Invoice.new.calculate_tax prints its two lines again. Whenever a constant name and a file path disagree, run bin/rails zeitwerk:check first; it reports every mismatch in one pass. The autoloading guide documents the naming rules, and the ActiveSupport::Concern API page documents the blocks.

Rails Is Built From Concerns

Open a model’s ancestors and the pattern is everywhere. On a default 8.1 app, ActiveRecord::Base has 74 ancestors, and 53 of them extend ActiveSupport::Concern:

Ruby
ancestors = ActiveRecord::Base.ancestors
ancestors.size # => 74
ancestors.count { |mod| mod.singleton_class.include?(ActiveSupport::Concern) } # => 53
ancestors.first # => ActionText::Encryption
ancestors.values_at(25, 47, 57, 68) # => [ActiveRecord::Callbacks, ActiveRecord::Validations, ActiveRecord::Persistence, Object]
Article.instance_method(:valid?).owner # => ActiveRecord::Validations
Article.instance_method(:valid?).super_method.owner # => ActiveModel::Validations

valid? is defined in ActiveRecord::Validations, which calls super into ActiveModel::Validations further along the chain: the same lookup rule as First and Second. The first entry, ActionText::Encryption, comes before ActiveRecord::Base itself because it is prepended, so prepend is in production Rails too. For a concern that only one model uses, Module#concerning defines it inline: concerning :Archiving do ... end inside Article creates Article::Archiving, a real concern in the ancestors, without a separate file.

Module and Mixin Errors: Causes and Fixes

Every message is copied from Ruby 3.4.10 or Rails 8.1.3.1, so you can match it against your terminal character for character. Ruby 3.4 prints straight quotes and Class#method frames; older versions print backticks and bare method names for the same errors.

wrong argument type Class (expected Module) (TypeError)

text
include_class.rb:5:in 'Module#include': wrong argument type Class (expected Module) (TypeError)

Cause: include, extend, and prepend accept modules only. Passing a class, usually because a helper was written with class when it should have been module, raises at the include line:

Ruby
class Timestamps
end
 
class Invoice
  include Timestamps
end

Fix: Change class Timestamps to module Timestamps. If the class is meant to be a parent, inherit from it instead: class Invoice < Timestamps. The mirror-image mistake, inheriting from a module, raises superclass must be an instance of Class (given an instance of Module) (TypeError); a module needs include.

undefined method 'tags' for an instance of Invoice (NoMethodError)

text
extend_mixup.rb:11:in '<main>': undefined method 'tags' for an instance of Invoice (NoMethodError)

Cause: The module was mixed in with the wrong verb. extend put tags on the Invoice class object, but the call is on an instance:

Ruby
module Taggable
  def tags
    []
  end
end
 
class Invoice
  extend Taggable
end
 
Invoice.new.tags

The twin message, undefined method 'tags' for class Invoice (NoMethodError), means the opposite: the module was included, and the method is being called on the class.

Fix: include for methods on instances, extend for methods on the class. When a module needs both, use an included hook with base.extend(ClassMethods), or class_methods do in a Rails concern.

uninitialized constant Billing::Invoice::TAX (NameError)

text
billing.rb:7:in 'Billing::Invoice#tax': uninitialized constant Billing::Invoice::TAX (NameError)

Cause: The class was defined with the compact class Billing::Invoice form, so Billing is not in Module.nesting and a bare TAX can’t be found:

Ruby
module Billing
  TAX = 0.2
end
 
class Billing::Invoice
  def tax
    TAX
  end
end
 
Billing::Invoice.new.tax

Fix: Nest the definition (module Billing around class Invoice) so the namespace is in scope, or qualify the constant as Billing::TAX.

cyclic include detected (ArgumentError)

text
cyclic.rb:9:in 'Module#append_features': cyclic include detected (ArgumentError)

Cause: Two modules include each other, so the ancestors chain would loop. The error is raised at the second include, in whichever file loads last:

Ruby
module Auditable
end
 
module Trackable
  include Auditable
end
 
module Auditable
  include Trackable
end

A module that includes itself raises the same message.

Fix: Break the cycle. Move the shared methods into a third module both can include, or let the class include both modules side by side and rely on the lookup order.

Cannot define multiple 'included' blocks for a Concern (ActiveSupport::Concern::MultipleIncludedBlocks)

text
/usr/local/bundle/gems/activesupport-8.1.3.1/lib/active_support/concern.rb:162:in 'ActiveSupport::Concern#included': Cannot define multiple 'included' blocks for a Concern (ActiveSupport::Concern::MultipleIncludedBlocks)
	from /work/demo/app/models/concerns/broken.rb:9:in '<module:Broken>'

Cause: A concern calls included do twice, often after a merge that brought two versions of the same block together. ActiveSupport::Concern stores one block per module and refuses a second:

Ruby
# app/models/concerns/broken.rb
module Broken
  extend ActiveSupport::Concern
 
  included do
    validates :title, presence: true
  end
 
  included do
    validates :slug, presence: true
  end
end

Fix: Merge the two into one included do block. The same rule applies to prepended do, which raises ActiveSupport::Concern::MultiplePrependBlocks.

expected file app/models/concerns/taggable.rb to define constant Taggable, but didn't (Zeitwerk::NameError)

text
(demo):1:in '<main>': expected file /work/demo/app/models/concerns/taggable.rb to define constant Taggable, but didn't (Zeitwerk::NameError)

Cause: Zeitwerk derives the expected constant from the file path, and the file defines something else. The two usual reasons are a misspelled constant, as in this file, and the pre-Zeitwerk Concerns:: prefix (module Concerns::Taggable), which fails with uninitialized constant Concerns (NameError) because app/models/concerns is an autoload root, not a namespace:

Ruby
# app/models/concerns/taggable.rb
module Tagable
  extend ActiveSupport::Concern
end

Fix: Make the constant match the path: app/models/concerns/taggable.rb defines Taggable, and app/models/billing/tax.rb defines Billing::Tax. The console form carries the absolute path; bin/rails zeitwerk:check prints the relative form in this heading and lists every mismatch at once.

undefined method 'before_validation' for module PlainPublishable (NoMethodError)

text
app/models/concerns/plain_sluggable.rb:4:in 'PlainSluggable.included': undefined method 'before_validation' for module PlainPublishable (NoMethodError)
    base.before_validation :set_slug
        ^^^^^^^^^^^^^^^^^^
	from app/models/concerns/plain_publishable.rb:3:in 'Module#include'

Cause: One plain-Ruby module includes another, and the inner module’s self.included hook calls model methods on base. That hook fires at the moment of inclusion, when base is the outer module, not the model:

Ruby
# app/models/concerns/plain_sluggable.rb
module PlainSluggable
  def self.included(base)
    base.before_validation :set_slug
  end
 
  def set_slug
    self.slug ||= title.to_s.parameterize
  end
end
Ruby
# app/models/concerns/plain_publishable.rb
module PlainPublishable
  include PlainSluggable
 
  def publish!
    update!(published_at: Time.current)
  end
end

Fix: Add extend ActiveSupport::Concern to both modules and move the base.before_validation call into an included do block. The concern module holds the inner block back until a class includes the outer module, so it runs against the model.

Wrapping Up

When a mixin does something unexpected, print ancestors before reading any more code: the chain says which method wins, where super goes, and whether a module was included, prepended, or extended onto the singleton class. On Rails, bin/rails zeitwerk:check answers the other half, whether the file and the constant agree. Between those two commands, most module mysteries take a minute to clear up.

P.S. If you’d like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!

Frequently asked questions

What is the difference between include and extend in Ruby?
include adds a module’s methods as instance methods and places the module in the class’s ancestors, so every instance gains them. extend adds the same methods to one object’s singleton class; on a class, that means class methods. Use include for instance behavior and extend for class-level helpers.
When should I use prepend instead of include in Ruby?
Use prepend when the module must run before the class’s own method of the same name. Ruby inserts a prepended module ahead of the class in ancestors, so its method is found first and can call super to reach the original. Prepend is the safe way to wrap existing methods.
What is ActiveSupport::Concern for in Rails?
ActiveSupport::Concern gives a module an included block that runs in the context of the including class, a class_methods block for class-level methods, and dependency resolution: when one concern includes another, both apply to the final class. It removes the base.extend and self.included boilerplate that plain Ruby mixins need.
How do I organize modules in a Rails app?
Put model mixins in app/models/concerns and controller mixins in app/controllers/concerns. Both are autoload roots, so app/models/concerns/taggable.rb must define Taggable, not Concerns::Taggable. Namespaced helpers go in lib, which Rails autoloads via config.autoload_lib (added in Rails 7.1, generated by default since), so lib/billing/tax.rb must define Billing::Tax.

Published , Updated

Wondering what you can do next?

  • Share this article on social media
Cleiviane Costa

Cleiviane Costa

Our guest author Cleiviane Costa is a software engineer with 10+ years of experience. She's passionate about Ruby on Rails and loves to write about things that she discovers in her day-to-day activities.

All articles by Cleiviane Costa

Become our next author!

Find out more
$appsignal install

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal