Lessons from Writebook - Concerns - Lesson 2 - A nice DSL

I have had the glorious pleasure of reading WriteBook - mainly for the benefit of understanding how the creators of Basecamp writes code.

They make use of certain styles. Notably concerns. Looks like they prefer nice DSLs over clumsy intermediate objects. e.g. like Viewer objects.

I want to conduct an exercise by asking myself this question: how would I edit the following code, adopting their paradigms? You may conduct the exercise yourself, and see what you produce.

# the original code.
# app/controllers/accounts_controller.rb
class AccountsController < ApplicationController
  def transfer
    @source_account = Account.find(params[:id])
    @destination_account = Account.find(params[:destination_id])
    @amount = params[:amount]

    if @source_account.transfer_to(@destination_account, @amount)
      flash[:success] = "Successfully transferred #{@amount} to #{@destination_account}"
      redirect_to @source_account
    else
      flash[:error] = "There was a problem transferring money to #{@destination_account}"
      render :transfer
    end
  end
end

Use of Scopes

  • e.g. in WriteBook, they make use of BookScoped. We can adopt a similar philsophy here - except we will call it AccountScoped.

module AccountScoped extend ActiveSupport::Concern
  included do
    before_action :set_account, :set_destination_account
  end

  private
    def set_account
      @account = Account.find(params[:account_id])
    end

    def set_destination_account
      @destination_account = Account.find(params[:destination_account_id])
    end       
end

# which produces the following so far:
# app/controllers/accounts_controller.rb
class AccountsController < ApplicationController
  include AccountScoped

  def transfer
    if @source_account.transfer_to(@destination_account, @amount)
      flash[:success] = "Successfully transferred #{@amount} to #{@destination_account}"
      redirect_to @source_account
    else
      flash[:error] = "There was a problem transferring money to #{@destination_account}"
      render :transfer
    end
  end
end

Use Restful controllers

DHH seems to prefer using Restful controllers, rather than proliferate custom actions in a single controller.

  • Now we are createing a transfer. This is non-idempotent.
# app/controllers/accounts/transfers_controller.rb
class Accounts::TransfersController < ApplicationController
  include AccountScoped

  def create
    if @source_account.transfer_to(@destination_account, @amount)
      flash[:success] = "Successfully transferred #{@amount} to #{@destination_account}"
      redirect_to @source_account
    else
      flash.now[:error] = "There was a problem transferring money to #{@destination_account}"      
    end
  end
end

Use Transfer Objects

module Transferrable extend ActiveSupport::Concern
  included do
    def transfer_to(destination_account)
      Account::Transfer.new(self, destination_account)
    end    
  end    
end

class Account
  class Transfer
    def initialize(source_account, destination_account)
      @source_account = source_account
      @destination_account = destination_account
    end


    def amount(x_dollars)      
      # we could use some type casting here
      # to ensure we get only dollars as opposed to strings
      @destination_account.balance += x_dollars
      @source_account.balance -= x_dollars
    end

    def save
      # we could also wrap this in a transaction and save both accounts
      # only if a successful transaction can occur, and if money exists
      # in the source account
      
      # I will leave this as an exercise to the reader
    end
  end
end

# and mix this into the account model

class Account < ActiveRecord::Base
  include Transferrable
end

# which allows you to do:

class Accounts::TransfersController < ApplicationController
  include AccountScoped

  def create
    if @source_account.transfer_to(@destination_account).amount(params[:amount]).save
      redirect_to @source_account, success: "Successfully transferred #{@amount} to #{@destination_account}"
    else
      flash.now[:error] = "There was a problem transferring money to #{@destination_account}"      
    end
  end
end

Great! Looks like we’ve cleaned up quite a bit.

We could probably improve the error rendering - i.e. to use validation errors to populate the problems with the transfer. The main point: with the use of a concerns, we have a nice(r) DSL (if you prefer it).

If I’ve made a mistake somewhere here, please let me know!

References

  • https://signalvnoise.com/posts/3372-put-chubby-models-on-a-diet-with-concerns
  • https://jeromedalbert.com/how-dhh-organizes-his-rails-controllers/

Helpful discussion:

  • https://discuss.rubyonrails.org/t/helping-devs-understand-concerns-faster/74619/20?u=benkoshy
  • https://discuss.rubyonrails.org/t/helping-devs-understand-concerns-faster/74619/21?u=benkoshy
  • https://discuss.rubyonrails.org/t/helping-devs-understand-concerns-faster/74619/22?u=benkoshy

Give me something to test!


class Account
  class Transfer
    def initialize(source_account, destination_account)
      @source_account = source_account
      @destination_account = destination_account
    end


    def amount(x_dollars)
      # we could use some type casting here
      # to ensure we get only dollars as opposed to strings
      @destination_account.balance += x_dollars
      @source_account.balance -= x_dollars
    end


    def to_s
      "Source account is: #{@source_account.balance}, and destination_account is #{@destination_account.balance}"
    end
  end
end


module Transferrable
  def self.included(base) # the same as using a concern, but no need for dependencies
      base.extend ClassMethods
      base.class_eval do
          def transfer_to(destination_account)
              Account::Transfer.new(self, destination_account)
          end
      end
  end

  module ClassMethods
      # nothing here
    end
end

class Account
  include Transferrable

  attr_accessor :balance

  def initialize
    self.balance = 100 # start off with $100 balance
  end
end

source = Account.new
destination = Account.new

transfer = source.transfer_to(destination)
puts transfer.to_s # view the balances thus far

transfer.amount(100)

puts transfer.to_s # view the balances after the transfer has taken effect
Written on May 14, 2025