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 #
| Aspect | Java / C# | Ruby |
|---|---|---|
| Syntax | interface Foo { void bar(); } | Module with raise NotImplementedError |
| Verification | At compile time (compiler) | At runtime (NoMethodError) |
| Multiple interfaces | implements A, B, C | include A; include B; include C |
| Default implementations | Java 8+: default methods | Always possible — module methods have bodies |
| Flexibility | Types must match exactly | Any object with the right methods |
| Type safety | High — checked by the compiler | Low — needs good test coverage |
| Testing needs | Can rely on the compiler | Good 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
NotImplementedErrorare 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 types —
play_sound(animal)works for any object with asoundmethod, 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 usemethod_missing— without it,respond_to?returns misleading results.- The
includedcallback 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.