Delegation
Sep 22, 2026When writing object oriented code, a pretty useful technique is delegation–letting one object figure things out by sending messages to another object. At my work, many teams prefer procedural code over OOP, and they often forgo delegation for constructing data upfront. Here’s an example!
# A very simplified domain for a movie purchasing website.
class Video
attr_accessor(:title, :s3_key, :distributor_id, :runtime)
end
class User
attr_accessor(:email, :is_internal_user)
end
class Invoice
attr_accessor(:cost, :email, :download_url, :due_date)
end
class VideoService
def self.buy_video(video, user)
invoice = Invoice.new
invoice.email = user.email
# A very important partner who gets their own S3 bucket.
if video.distributor_id == 123
invoice.download_url = S3Client.get_url('special_bucket', video.s3_key)
else
invoice.download_url = S3Client.get_url('regular_bucket', video.s3_key)
end
invoice.cost = Distributor.find(video.distributor_id).price
# Short video discount.
if video.runtime < 30.minutes
invoice.cost = invoice.cost / 2
end
# Internal users who are buying content for testing don't have to pay!
if user.is_internal_user
invoice.due_date = :never
else
invoice.due_date = Time.now + 30.days
end
EmailClient.send_invoice(invoice)
end
end
Here we’re making a buy_video method that constructs and sends an invoice to a user that just bought a video. The “objects” here are just structs. All the behavior is in the buy_video method. This is pretty typical of a lot of procedural code I’ve seen at work.
I dunno about you, but I find this pretty hard to read. buy_video is a very complicated method! And it’s complicated in the worst way–it does different things in the same place.
I think delegation can offer us some solutions here. Check out this code:
class Video
attr_accessor(:title, :s3_key, :distributor_id, :runtime)
def short?
runtime < 30.minutes
end
def price
Distributor.find(distrubtor_id).price
end
def s3_bucket
if distributor_id == 123
'special_bucket'
else
'regular_bucket'
end
end
end
class User
attr_accessor(:email, :is_internal_user)
def internal?
is_internal_user
end
end
class Invoice
def initialize(video, user)
@video = video
@user = user
end
def email
@user.email
end
def download_url
S3Client.get_url(@video.s3_bucket, @video.s3_key)
end
def cost
if @video.short?
@video.price / 2
else
@video.price
end
end
def due_date
if @user.internal?
:never
else
Time.now + 30.days
end
end
end
class VideoService
def self.buy_video(video, user)
EmailClient.send_invoice(Invoice.new(video, user))
end
end
Here instead of constructing Invoice upfront, we let Invoice delegate to Video and User. And now instead of having all of our logic inside one method, it’s spread around into places where it makes sense.
Delegation is a powerful tool!