Reason #140 • May 20th, 2026

Modules as singletons

The singleton pattern in object-oriented programming is a design pattern that restricts the instantiation of a class to a single instance and provides a global point of access to that instance. In Ruby, this pattern can be implemented using classes, but it can also be achieved more simply using modules. Since modules are objects, they are effectively singletons by default!

A beautiful way of implementing a module as a singleton is by using extend self. Here's an example of one of my favorite use cases for this pattern: a simple HTTP client.

Ruby
require "net/http"
require "json"

module Typicode
  extend self

  BASE_URI = URI("https://jsonplaceholder.typicode.com")
  HTTP_ERROR = Class.new(StandardError)

  def get(path) = request("GET", path)
  def post(path, params = nil) = request("POST", path, params)
  def patch(path, params = nil) = request("PATCH", path, params)
  def put(path, params = nil) = request("PUT", path, params)
  def delete(path) = request("DELETE", path)

  private

  def request(method, path, params = nil)
    response = Net::HTTP.start(BASE_URI.host, BASE_URI.port, use_ssl: true) do |http|
      http.send_request(method, path, params&.to_json, headers)
    end

    if response.is_a?(Net::HTTPSuccess)
      JSON.parse(response.body)
    else
      raise HTTP_ERROR, "#{response.code} for #{method} /#{path}"
    end
  end

  def headers
    @headers ||= {
      "Content-Type" => "application/json",
      "Accept" => "application/json",
    }.freeze
  end
end

Typicode.get("/posts/1")
# => { "id" => 1, "title" => "sunt aut facere...", ... }

Typicode.post("/posts", title: "foo", body: "bar", userId: 1)
# => { "id" => 101, "title" => "foo", "body" => "bar", ... }

Typicode.patch("/posts/1", title: "updated title")
# => { "id" => 1, "title" => "updated title", ... }

Typicode.delete("/posts/1")
# => {}

Typicode.get("/invalid")
# => raises Typicode::HTTP_ERROR: "404 for GET /invalid"
    

What extend self does is make all instance methods of the module also available as module methods. This saves us the trouble of having to prefix all our method definitions with self. to make them module methods. It also allows private to work as expected, which is not the case for module methods defined with self..

Another common way to achieve the same effect is by adding a class << self block and defining the methods inside it, but I tend to prefer the more minimal extend self, especially since it avoids another level of indentation.

Note that we can use instance variables in our module as we would in any other object, so our singletons can still have some internal state if we need it. We have to be mindful of thread safety if they are used in a multi-threaded context.

BTW, I only used memoization of headers in the example to demonstrate that we can have some internal state in our module, it could have been a constant instead. In real life, I commonly find myself merging ENV variables into the headers though, e.g. API tokens, at which point memoization makes more sense.

History

As both extend and self have been part of Ruby since its inception, so has extend self.