Jun 10, 2026
Presenter Pattern in Rails: SimpleDelegator, Draper, or Just Helpers?
When one model grows a dozen view-shaped methods, you have three options. Here's how I pick between SimpleDelegator, Draper, and plain helpers — and where ViewComponent draws the line.
A Job model with formatted_salary, posted_ago_in_words, country_flag_emoji, tier_badge_class, and tooltip_text on it is a model that’s quietly turned into a view. Those methods don’t change records. They format them. They belong somewhere closer to the template.
The question is where. Rails gives you at least three answers, and I’ve used all three in production. Here’s how I decide.
The shape of the problem
I’ll use a job board model, since that’s what I work on every day:
# illustrative
class Job < ApplicationRecord
belongs_to :company
def formatted_salary
return "Not disclosed" unless salary_min
"#{salary_currency_symbol}#{salary_min / 1000}k–#{salary_max / 1000}k"
end
def posted_ago
"#{((Time.current - posted_at) / 1.day).to_i}d ago"
end
def country_flag
COUNTRY_FLAGS[country_code] || ""
end
def tooltip_text
"Posted #{posted_ago} on #{source_name}"
end
end
Five view-shaped methods. None of them touch the database. None of them are valid in a JSON serializer or an API context — they’re for HTML. They’ve snuck into the model because the template was the path of least resistance.
Option 1: A SimpleDelegator subclass
The lightest possible presenter is one Ruby file:
# illustrative
require "delegate"
class JobPresenter < SimpleDelegator
def initialize(job, view)
super(job)
@view = view
end
def formatted_salary
return "Not disclosed" unless salary_min
"#{salary_currency_symbol}#{salary_min / 1000}k–#{salary_max / 1000}k"
end
def posted_ago
@view.time_ago_in_words(posted_at) + " ago"
end
def country_flag
COUNTRY_FLAGS[country_code] || ""
end
end
Then in the controller or view:
# illustrative
@job = JobPresenter.new(Job.find(params[:id]), view_context)
That’s it. No gem. No conventions to learn. The presenter forwards every method it doesn’t define to the wrapped model, so @job.title still works in the template. You get access to view helpers via the injected view (or view_context).
I reach for this when I have one or two models that need presentation logic and I don’t want a dependency. The cost is that you’re hand-rolling the lookup pattern (controller has to wrap, every collection action has to map).
Option 2: The draper gem
draper formalizes the SimpleDelegator pattern and adds the conveniences:
# illustrative
class JobDecorator < Draper::Decorator
delegate_all
def formatted_salary
return "Not disclosed" unless object.salary_min
h.number_to_currency(object.salary_min, ...)
end
def posted_ago
h.time_ago_in_words(object.posted_at) + " ago"
end
end
Then Job.find(params[:id]).decorate returns the decorated instance, Job.all.decorate decorates the collection, and h gives you helpers without injecting view_context yourself.
Where Draper earns its keep:
- You have 5+ models with presentation logic. The convention pays for itself.
- You’re building admin UIs where every model has a “display name”, “status badge”, “edit link” pattern.
- Your team already knows it. Draper has been around long enough that “I’ll write a decorator” is unambiguous.
Where I skip it:
- The app is small. One file in
app/presenters/is fewer moving parts than a gem. - You’ve already adopted ViewComponent (more on this below).
Option 3: Plain helper modules
Rails’ default answer:
# illustrative
module JobsHelper
def formatted_job_salary(job)
return "Not disclosed" unless job.salary_min
"#{job.salary_currency_symbol}#{job.salary_min / 1000}k–#{job.salary_max / 1000}k"
end
def job_posted_ago(job)
"#{time_ago_in_words(job.posted_at)} ago"
end
end
Helpers are global. That’s their feature and their flaw. Every helper module gets mixed into every view. formatted_salary(job) clashes with formatted_salary(invoice) unless you prefix every method with the model name — which is what I always end up doing.
I use helpers for truly app-wide formatting (formatted_money, inline_svg, external_link_to) and almost never for model-specific presentation anymore. The prefix pollution is a real cost, and helpers can’t carry state — every method takes the object as its first argument, every time.
The boundary with ViewComponent
I covered when ViewComponent is overkill in 01 — Decorator pattern in Rails. The short version of the boundary:
- Presenter: wraps a model, exposes formatted attributes. Output is strings/booleans/CSS classes. No template.
- ViewComponent: encapsulates a slice of HTML, takes any inputs, renders a template. Output is markup.
A JobCard is a ViewComponent — it owns a chunk of HTML with conditional classes, slots for actions, the works. A JobPresenter is the thing you pass into the JobCard, so the component doesn’t have to know that formatted_salary is “Not disclosed” when salary_min is nil.
You use both. They’re not alternatives.
How I actually pick
A flowchart that lives in my head:
- One model, ≤3 view methods? Leave them on the model. Yes, mixing concerns. Yes, fine.
- One or two models, more methods than that? Hand-rolled
SimpleDelegatorinapp/presenters/. No gem. - Five-plus models, app-wide pattern, team familiar with Draper? draper.
- The “presentation logic” is actually “this chunk of UI”? ViewComponent.
- Truly cross-cutting formatting? Helper module, prefix the method names.
The mistake I see most often is jumping to (3) before (2). A 200-line Ruby file in app/presenters/job_presenter.rb covers most apps for years. You can always promote it to a Draper decorator later if you grow into the convention. Going the other direction — ripping Draper out — is less fun.
The thing about presenters is that they’re a destination for view logic, not a magic word. Wherever you put it, the win is the same: the model stops carrying methods that only make sense inside an ERB tag, and the template stops calling if job.salary_min.present? && job.salary_max.present? three times in a row.