Cleaner Mailer Views in Rails with the Presenter Pattern

| Shey Sewani | Toronto

I love the presenter pattern. I use it in all my mailer views.

It’s deceptively simple, but incredibly useful. The idea is: move formatting and conditionals out of the view and into a plain Ruby object. That keeps logic in one place and makes the view easier to read.

I once shared it with a colleague, and they were genuinely blown away. Hopefully, you’ll find it just as useful.

An unrefactored mailer view

This view has some issues that make it a good candidate for refactoring:

  • It uses inline date formatting (strftime) right in the template.
  • It calculates days_left directly inside the view logic, tying it to Date.today at render time.
  • It contains conditional branching that controls not just structure, but wording.
  • It handles pluralization manually: "day#{'s' unless days_left == 1}".
Hi <%= @user.name %>,

Just a reminder that your book "<%= @book.title %>" is due on
<%= @book.due_date.strftime("%B %d, %Y") %>.

<% days_left = (@book.due_date - Date.today).to_i %>
<% if days_left <= 2 %>
  Please return it as soon as possible.

  It's due in just <%= days_left %> day<%= 's' if days_left != 1 %>!
<% else %>
  You still have <%= days_left %> day<%= 's' if days_left != 1 %> left.
<% end %>

Thanks,

Your Friendly Library

Same logic, cleaner view

The refactor moved the conditionals, formatting, and pluralization out of the view and into a new presenter class. The result is a clean template with no conditional logic or formatting.

Hi <%= @presenter.user_name %>,

Just a reminder that your book "<%= @presenter.book_title %>" is due on <%= @presenter.due_date %>.

<%= @presenter.return_message %>

Thanks,

Your Friendly Library

How to get there

The new presenter class.

class BookDueSoonPresenter
  attr_reader :user, :book

  def initialize(user:, book:)
    @user = user
    @book = book
  end

  def user_name
    user.name
  end

  def subject
    "Reminder: Book Due Soon"
  end

  def book_title
    book.title
  end

  def due_date
    book.due_date.strftime("%B %d, %Y")
  end

  def days_left
    (book.due_date.to_date - Date.today).to_i
  end

  def return_message
    messages.fetch(message_key) % { days: pluralized_days_left }
  end

  def message_key
    days_left <= 2 ? :urgent : :friendly
  end

  def messages
    {
      urgent:   "Please return it as soon as possible — it's due in just %{days}!",
      friendly: "You still have %{days} left."
    }
  end

  def pluralized_days_left
    "#{days_left} #{'day'.pluralize(days_left)}"
  end
end

Instantiating the presenter in the mailer.

class UserMailer < ApplicationMailer
  def book_due_soon(user, book)
    @presenter = BookDueSoonPresenter.new(user: user, book: book)
    mail(to: user.email, subject: @presenter.subject )
  end
end

Testing Before vs After

Before the refactor, testing required parsing rendered email output with string matching or regexes — a fragile way to verify logic.

test "return_message is urgent when due in 1 day" do
  user = User.new(name: "Shey")
  book = Book.new(title: "Dune", due_date: Date.today + 1)

  email = UserMailer.book_due_soon(user, book).body.to_s

  assert_includes email, "it's due in just 1 day!"
end

After the Refactor: test the presenter directly instead of the view.

test "return_message is urgent when due in 1 day" do
  user = User.new(name: "Shey")
  book = Book.new(title: "Dune", due_date: Date.today + 1)

  presenter = BookDueSoonPresenter.new(user: user, book: book)

  assert_equal "Please return it as soon as possible — it's due in just 1 day!", presenter.return_message
end

It works the same way in controller views as it does in mailers — anywhere you have complex logic in templates.

Hope you found it helpful.