Reason #138 • May 18th, 2026

You don't need dependency injection

In statically typed OOP languages, dependency injection is necessary to achieve testability in many cases. In Ruby, however, the language's dynamic nature and open classes allow us to achieve the same testability without introducing the indirection of dependency injection.

Consider an Order that emails a confirmation through a Mailer:

Ruby
class Mailer
  def self.send(to:, subject:, body:)
    # ... actually delivers email ...
  end
end

class Order
  attr_reader :id, :email

  def initialize(id, email)
    @id = id
    @email = email
  end

  def process
    Mailer.send(
      to: email,
      subject: "Order #{id} confirmed",
      body: "Thanks for your order!",
    )
    :ok
  end
end
      
Java
interface Mailer {
  void send(String to, String subject, String body);
}

class SmtpMailer implements Mailer {
  public void send(String to, String subject, String body) {
    // ... actually delivers email ...
  }
}

class Order {
  private final int id;
  private final String email;
  private final Mailer mailer;

  Order(int id, String email, Mailer mailer) {
    this.id = id;
    this.email = email;
    this.mailer = mailer;
  }

  String process() {
    mailer.send(
      email,
      "Order " + id + " confirmed",
      "Thanks for your order!"
    );
    return "ok";
  }
}
      

Note how the Java version requires a Mailer interface, a concrete SmtpMailer, and an Order constructor that accepts a Mailer. In Ruby we just call out to Mailer directly.

To test the Ruby version without actually sending email, we replace Mailer.send for the duration of the test rather than fiddle with dependency injection:

Ruby
require "minitest/autorun"

class OrderTest < Minitest::Test
  def test_process_sends_confirmation
    captured = nil
    original = Mailer.method(:send)
    Mailer.define_singleton_method(:send) do |**args|
      captured = args
      :delivered
    end

    order = Order.new(42, "[email protected]")
    assert_equal :ok, order.process
    assert_equal "[email protected]", captured[:to]
    assert_equal "Order 42 confirmed", captured[:subject]
  ensure
    Mailer.singleton_class.define_method(:send, original) if original
  end
end
      
Java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class OrderTest {
  static class FakeMailer implements Mailer {
    String to, subject, body;

    public void send(String to, String subject, String body) {
      this.to = to;
      this.subject = subject;
      this.body = body;
    }
  }

  @Test
  void processSendsConfirmation() {
    FakeMailer fake = new FakeMailer();
    Order order = new Order(42, "[email protected]", fake);

    assertEquals("ok", order.process());
    assertEquals("[email protected]", fake.to);
    assertEquals("Order 42 confirmed", fake.subject);
  }
}
      

In real Ruby projects we would be using minitest-mock or a similar library to handle the stubbing and expectations more cleanly, but in this example I wanted to show the underlying mechanics of stubbing in raw Ruby.

Note: There may still be cases when dependency injection makes sense, such as when you genuinely have a need for a "plug and play" kind of architecture. But you don't have to complicate your application code just for the sake of testability!

History

The ability to redefine class methods at runtime has been a core feature of Ruby since its inception.

Object#define_singleton_method was added in Ruby 1.9.2 (2010) as a cleaner alternative to the older class << obj; def ...; end; end idiom.