Interfaces #

Developers coming from Java, C#, or TypeScript usually look for an interface keyword in Ruby — and don’t find one. This isn’t a deficiency, but a deliberate design choice. Ruby embraces a fundamentally different philosophy: rather than enforcing contracts through static types, Ruby relies on duck typing — if an object has the methods you need, it can be used anywhere without needing to be declared as an implementor of a specific interface. This article explains how Ruby replicates the benefits of interfaces through modules, the NotImplementedError pattern, respond_to?, and duck typing — and when each approach is most appropriate.

Why Ruby Has No Formal Interface #

Before discussing Ruby’s solutions, it’s important to understand why interfaces exist in Java and C#: because those languages are statically typed — every variable’s type must be known at compile time. Interfaces are a way to state “any object entering here must have methods X and Y” so the compiler can verify it.

Ruby is dynamically typed — types aren’t verified at compile time, only at runtime. This means Ruby doesn’t need interfaces for the same reason. All that’s required is: when a method is called, the object in question must have that method. If not, Ruby throws NoMethodError — and that’s Ruby’s version of a “type error”.

# In Java, you MUST declare that Dog implements Animal
# interface Animal { void speak(); }
# class Dog implements Animal { public void speak() { ... } }

# In Ruby, you don't need any declaration
class Dog
  def sound
    "Woof!"
  end
end

class Cat
  def sound
    "Meow!"
  end
end

class Bird
  def sound
    "Chirp!"
  end
end

# This function works with ALL the objects above — no interface needed
def play_sound(animal)
  puts animal.sound
end

play_sound(Dog.new)  # => Woof!
play_sound(Cat.new)  # => Meow!
play_sound(Bird.new)  # => Chirp!

This is duck typing: “if it can make sounds like an animal, it’s an animal” — no official interface certificate required.


Modules as Behavioral Contracts #

Although Ruby has no formal interface, modules can be used to define more explicit behavioral contracts. There are two ways modules serve as interface replacements: as a documentary contract (telling developers what to implement) and as an enforced contract (raising errors when methods aren’t implemented).

The NotImplementedError Pattern — Enforced Contract #

The most common pattern for replicating an interface in Ruby is defining methods in a module that raise NotImplementedError. These methods act as placeholders telling developers: “you must override this method in the class that includes this module.”

module Persistable
  def save
    raise NotImplementedError, "#{self.class}#save must be implemented"
  end

  def delete
    raise NotImplementedError, "#{self.class}#delete must be implemented"
  end

  def find(id)
    raise NotImplementedError, "#{self.class}.find must be implemented"
  end
end

# Implementation for a SQL database
class UserRepository
  include Persistable

  def save(user)
    DB.execute("INSERT INTO users (name, email) VALUES (?, ?)",
               user.name, user.email)
    puts "#{user.name} saved to PostgreSQL"
  end

  def delete(id)
    DB.execute("DELETE FROM users WHERE id = ?", id)
    puts "User #{id} deleted from PostgreSQL"
  end

  def find(id)
    DB.query("SELECT * FROM users WHERE id = ?", id)
  end
end

# Implementation for in-memory storage (useful for testing)
class UserMemoryRepository
  include Persistable

  def initialize
    @store = {}
    @next_id = 1
  end

  def save(user)
    @store[@next_id] = user
    @next_id += 1
    puts "#{user.name} saved to memory"
  end

  def delete(id)
    @store.delete(id)
    puts "User #{id} deleted from memory"
  end

  def find(id)
    @store[id]
  end
end

# A class that forgets to implement the methods — caught immediately at runtime
class BrokenRepository
  include Persistable
  # forgot to implement save, delete, find
end

repo = BrokenRepository.new
repo.save(nil)
# => NotImplementedError: BrokenRepository#save must be implemented

Modules with Default Implementations #

One advantage of Ruby modules over Java interfaces is that modules can provide default implementations for certain methods. This is similar to default methods in Java 8+, but more natural in Ruby:

module Exportable
  # Method that MUST be implemented by the class
  def to_hash
    raise NotImplementedError, "#{self.class}#to_hash must be implemented"
  end

  # Methods with DEFAULT implementations — not required to override
  def to_json
    require 'json'
    to_hash.to_json
  end

  def to_csv_row
    to_hash.values.join(",")
  end

  def to_xml
    pairs = to_hash.map { |k, v| "<#{k}>#{v}</#{k}>" }.join
    "<#{self.class.name.downcase}>#{pairs}</#{self.class.name.downcase}>"
  end
end

class Product
  include Exportable

  attr_reader :name, :price, :category

  def initialize(name, price, category)
    @name     = name
    @price    = price
    @category = category
  end

  # Only to_hash is required — the rest comes free from the module
  def to_hash
    { name: @name, price: @price, category: @category }
  end
end

class User
  include Exportable

  attr_reader :name, :email

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

  def to_hash
    { name: @name, email: @email }
  end
end

p = Product.new("Laptop", 15_000_000, "Electronics")
puts p.to_json      # => {"name":"Laptop","price":15000000,"category":"Electronics"}
puts p.to_csv_row   # => Laptop,15000000,Electronics
puts p.to_xml       # => <product><name>Laptop</name>...</product>

u = User.new("Rina", "[email protected]")
puts u.to_json      # => {"name":"Rina","email":"[email protected]"}

Duck Typing — Ruby’s Core Philosophy #

Duck typing isn’t just “not having interfaces” — it’s a fundamentally different approach to polymorphism. Instead of checking an object’s type, you check its capabilities:

# Type-based approach (ANTI-PATTERN in Ruby)
def process_payment(payment_method)
  if payment_method.is_a?(CreditCard)
    payment_method.charge_card(total)
  elsif payment_method.is_a?(BankTransfer)
    payment_method.bank_transfer(total)
  elsif payment_method.is_a?(Ewallet)
    payment_method.deduct_balance(total)
  end
end

# Duck typing approach (CORRECT in Ruby)
def process_payment(payment_method, total)
  payment_method.pay(total)   # every payment method must respond to :pay
end

# Each class just needs to have a :pay method
class CreditCard
  def pay(total)
    puts "Charging Rp #{total} to credit card"
    true
  end
end

class BankTransfer
  def pay(total)
    puts "Transferring Rp #{total} to the destination account"
    true
  end
end

class DigitalWallet
  def pay(total)
    puts "Deducting Rp #{total} from wallet balance"
    true
  end
end

class CryptoCoin
  def pay(total)
    puts "Converting and paying Rp #{total} in crypto"
    true
  end
end

# Everything works without interfaces, without formal declarations
[CreditCard, BankTransfer, DigitalWallet, CryptoCoin].each do |klass|
  process_payment(klass.new, 150_000)
end
flowchart TD
    A[Payment Method] --> B{How to verify\nan object can be used?}
    B --> C["Java/C# approach\nFormal interface\n+ compilation"]
    B --> D["Ruby approach\nDuck Typing\nruntime check"]
    C --> C1["class CreditCard\nimplements Payment"]
    D --> D1["Any object that\nhas a .pay method"]
    C1 --> E["Verified at\ncompile time"]
    D1 --> F["Verified at\nruntime — NoMethodError\nif missing"]
    E --> G["Strict, safe\nbut rigid"]
    F --> H["Flexible, expressive\nbut needs good tests"]

respond_to? — Explicit Duck Typing #

Sometimes you need to explicitly check whether an object has a certain method before calling it — especially when the method is optional:

def render(content)
  # Check the object's capabilities, not its type
  if content.respond_to?(:to_html)
    puts content.to_html
  elsif content.respond_to?(:to_s)
    puts "<p>#{content.to_s}</p>"
  else
    puts "<p>Content cannot be rendered</p>"
  end
end

class HtmlArticle
  def to_html
    "<article><h1>Article</h1><p>Article content...</p></article>"
  end
end

class PlainText
  def to_s
    "This is plain text without HTML"
  end
end

render(HtmlArticle.new)  # => <article>...</article>
render(PlainText.new)    # => <p>This is plain text without HTML</p>
render(42)               # => <p>42</p>  (Integer has to_s)

respond_to_missing? — Dynamic Methods #

When a class uses method_missing to handle methods dynamically, respond_to? won’t detect them automatically. Define respond_to_missing? to keep them consistent:

class FlexibleProxy
  def initialize(target)
    @target = target
  end

  def method_missing(name, *args, &block)
    if @target.respond_to?(name)
      @target.send(name, *args, &block)
    else
      super
    end
  end

  # MUST be defined if you use method_missing
  def respond_to_missing?(name, include_private = false)
    @target.respond_to?(name, include_private) || super
  end
end

class Calculator
  def add(a, b) = a + b
  def multiply(a, b)  = a * b
end

proxy = FlexibleProxy.new(Calculator.new)

puts proxy.add(3, 4)              # => 7
puts proxy.respond_to?(:add)      # => true  (thanks to respond_to_missing?)
puts proxy.respond_to?(:nonexistent)   # => false

Stricter Interface Patterns #

When you want to ensure all required methods are implemented — not just when a method is called, but immediately when a class includes the module — you can use the included callback or self.included:

module StrictInterface
  def self.included(klass)
    # This callback runs when the module is included into a class
    klass.instance_variable_set(:@required_methods, [])
    klass.extend(ClassMethods)
  end

  module ClassMethods
    def required_methods(*method_names)
      @required_methods = method_names

      # Add a hook to verify after the class finishes being defined
      TracePoint.trace(:end) do |tp|
        next unless tp.self == self

        not_implemented = @required_methods.reject do |m|
          method_defined?(m) && instance_method(m).owner == self
        end

        unless not_implemented.empty?
          raise NotImplementedError,
            "#{self} hasn't implemented: #{not_implemented.join(', ')}"
        end

        tp.disable
      end
    end
  end
end

The approach above is quite complex for production. A simpler and more commonly used alternative in the Ruby community is the abstract_method pattern simulated with raise:

module Notification
  # Define "abstract methods" that must be implemented
  def send(to, message)
    raise NotImplementedError, <<~MSG
      #{self.class}#send hasn't been implemented.
      Classes that include Notification must define:
        def send(to, message)
          # notification sending implementation
        end
    MSG
  end

  # Another "abstract method"
  def delivery_status(notification_id)
    raise NotImplementedError, "#{self.class}#delivery_status must be implemented"
  end

  # Methods with default implementations — not required to override
  def send_bulk(recipient_list, message)
    recipient_list.map { |recipient| send(recipient, message) }
  end

  def can_send?
    true   # default: always can send; override if there are conditions
  end
end

class EmailNotification
  include Notification

  def send(recipient, message)
    puts "Email to #{recipient}: #{message}"
    { status: :sent, channel: :email, recipient: recipient }
  end

  def delivery_status(id)
    puts "Checking email status ##{id}"
    :sent
  end
end

class SMSNotification
  include Notification

  def send(recipient, message)
    # SMS can only be 160 characters
    truncated = message[0..159]
    puts "SMS to #{recipient}: #{truncated}"
    { status: :sent, channel: :sms, recipient: recipient }
  end

  def delivery_status(id)
    :sent
  end

  def can_send?
    # Check the SMS quota
    remaining_quota > 0
  end

  private

  def remaining_quota
    1000   # simplified
  end
end

class PushNotification
  include Notification

  def send(recipient, message)
    puts "Push notification to device #{recipient}: #{message}"
    { status: :queued, channel: :push, recipient: recipient }
  end

  def delivery_status(id)
    :queued
  end
end

# Polymorphism — all treated the same
notifications = [EmailNotification.new, SMSNotification.new, PushNotification.new]

notifications.each do |n|
  n.send("[email protected]", "Your order has been shipped!")
end

# send_bulk works for all because the module provides a default implementation
email = EmailNotification.new
email.send_bulk(["[email protected]", "[email protected]", "[email protected]"], "Year-end promo!")

Polymorphism Through Shared Interfaces #

The power of duck typing and modules as interfaces is most visible when you write code that doesn’t care about an object’s concrete type — only its capabilities:

module Renderable
  def render
    raise NotImplementedError, "#{self.class}#render must be implemented"
  end

  def render_with_wrapper(tag)
    "<#{tag}>#{render}</#{tag}>"
  end
end

class TextComponent
  include Renderable

  def initialize(text)
    @text = text
  end

  def render
    "<p>#{@text}</p>"
  end
end

class ImageComponent
  include Renderable

  def initialize(src, alt)
    @src = src
    @alt = alt
  end

  def render
    "<img src='#{@src}' alt='#{@alt}' />"
  end
end

class ButtonComponent
  include Renderable

  def initialize(label, url)
    @label = label
    @url   = url
  end

  def render
    "<a href='#{@url}' class='btn'>#{@label}</a>"
  end
end

class WebPage
  def initialize
    @components = []
  end

  def add(component)
    raise ArgumentError, "#{component.class} cannot be rendered" unless component.respond_to?(:render)
    @components << component
    self
  end

  def render_all
    @components.map(&:render).join("\n")
  end
end

page = WebPage.new
page
  .add(TextComponent.new("Welcome to our store!"))
  .add(ImageComponent.new("/banner.jpg", "Promo Banner"))
  .add(ButtonComponent.new("Shop Now", "/products"))

puts page.render_all
# => <p>Welcome to our store!</p>
# => <img src='/banner.jpg' alt='Promo Banner' />
# => <a href='/products' class='btn'>Shop Now</a>

Comparison: Ruby vs Java/C# for Interfaces #

AspectJava / C#Ruby
Syntaxinterface Foo { void bar(); }Module with raise NotImplementedError
VerificationAt compile time (compiler)At runtime (NoMethodError)
Multiple interfacesimplements A, B, Cinclude A; include B; include C
Default implementationsJava 8+: default methodsAlways possible — module methods have bodies
FlexibilityTypes must match exactlyAny object with the right methods
Type safetyHigh — checked by the compilerLow — needs good test coverage
Testing needsCan rely on the compilerGood tests are essential
# Ruby's advantage: objects from third-party libraries that don't include
# your module can still be used if they have the right methods

require 'ostruct'

# OpenStruct from the standard library — doesn't include any module
data = OpenStruct.new(render: "<span>OpenStruct can be rendered!</span>")

page = WebPage.new
page.add(data)   # works! because data.respond_to?(:render) => true
puts page.render_all
# => <span>OpenStruct can be rendered!</span>

When to Use Modules as Interfaces vs Pure Duck Typing #

Use modules with NotImplementedError if:
  ✓ You're building a library or gem used by others
  ✓ You need clear documentation of the "contract" to be fulfilled
  ✓ There are many implementations and you want consistency
  ✓ Required methods aren't obvious — developers need guidance

Pure duck typing without modules is fine if:
  ✓ Internal code only used by your own team
  ✓ The required methods are already obvious (to_s, each, call)
  ✓ Objects from third-party libraries already have the right methods
  ✓ Simple contexts where module overhead isn't worth it

Use respond_to? if:
  ✓ The required method is optional — there's fallback behavior
  ✓ You want to support objects from various sources without forcing module inclusion
  ✓ Metaprogramming or proxy patterns

Summary #

  • Ruby has no formal interface — and that’s deliberate — duck typing is a core philosophy that’s more flexible than static interfaces.
  • Modules with NotImplementedError are the idiomatic way to define behavioral contracts — they give implementors clear guidance without needing a compiler.
  • Modules can have default implementations — a big advantage over traditional Java interfaces; including classes only need to implement what truly needs customization.
  • Duck typing: check capabilities, not typesplay_sound(animal) works for any object with a sound method, with no formal declaration.
  • respond_to? for explicit duck typing — use it when methods are optional and there are distinct fallback behaviors.
  • respond_to_missing? must be defined if you use method_missing — without it, respond_to? returns misleading results.
  • The included callback lets a module run code the first time it’s included — useful for validation, configuration, or adding class methods automatically.
  • Duck typing needs good tests — the absence of compiler checks means you have to replace them with solid test coverage.
  • Third-party objects can participate — duck typing’s biggest advantage: libraries that know nothing about your module still work as long as they have the right methods.

← Previous: Classes   Next: Exceptions →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact