The Builder Pattern in Ruby

· 7 min read · Updated March 31, 2026 · intermediate
ruby builder-pattern design-patterns guides

The Builder Pattern in Ruby

The builder pattern is a creational design pattern that separates the construction of a complex object from its representation. Instead of initializing an object with a constructor that takes many arguments — some optional, some required — you use a dedicated builder object that constructs the target step by step. This gives you readable code, validation at each step, and flexibility to construct variations of the same object without multiplying constructors.

In this guide you will learn what the builder pattern solves, how to implement it with fluent interfaces, how to use blocks with instance_eval for nicer syntax, and how to build nested structures like HTML or XML documents. You will also see how to validate inputs during construction.

What Problem Does the Builder Pattern Solve?

Consider a User class with several optional fields:

class User
  attr_accessor :first_name, :last_name, :email, :phone, :address, :date_of_birth, :avatar_url, :role
end

With a traditional constructor, you might do this:

user = User.new
user.first_name = "Alice"
user.last_name = "Smith"
user.email = "alice@example.com"
user.role = "admin"

This works but is verbose and scattered. You could use a constructor with default values:

user = User.new(
  first_name: "Alice",
  last_name: "Smith",
  email: "alice@example.com",
  role: "admin"
)

But as optional fields grow, this becomes unwieldy. What if you need to validate that email looks like an email address, or that date_of_birth is in the past? You end up cramming validation logic into the initializer or spreading it across your codebase.

The builder pattern solves this by giving you a dedicated object whose sole job is to construct another object. Each builder method can validate its input and accumulate state until you call a final #build method.

A Basic Builder

Here is a simple builder for the User class:

class UserBuilder
  def initialize
    @user = User.new
  end

  def first_name(name)
    @user.first_name = name
    self
  end

  def last_name(name)
    @user.last_name = name
    self
  end

  def email(email)
    raise ArgumentError, "Invalid email format" unless email.include?("@")
    @user.email = email
    self
  end

  def role(role)
    @user.role = role
    self
  end

  def build
    raise ArgumentError, "first_name is required" if @user.first_name.nil?
    raise ArgumentError, "email is required" if @user.email.nil?
    @user
  end
end

Notice that each setter returns self. This enables method chaining, also known as a fluent interface:

user = UserBuilder.new
  .first_name("Alice")
  .last_name("Smith")
  .email("alice@example.com")
  .role("admin")
  .build

The builder reads almost like a sentence. Each step is explicit, and validation happens at the point where the data is set, not somewhere buried in a constructor.

Separating Construction from Representation

One of the core ideas behind the builder pattern is that the builder and the product are separate objects. The UserBuilder knows how to construct a User, but it is not the User itself. This separation has several benefits:

  • The product object (User) stays clean and focused on its domain behavior
  • The builder can have its own internal state and validation logic
  • You can have multiple builders for the same product, each tailored to a different use case

For example, you might have a UserBuilder for the public signup flow and a UserAdminBuilder for creating users via an admin panel. Both produce User objects, but they have different required fields and defaults:

class UserAdminBuilder < UserBuilder
  def initialize
    super
    @user.role = "member" # default role
  end

  def as_admin
    @user.role = "admin"
    self
  end
end

user = UserAdminBuilder.new
  .first_name("Bob")
  .last_name("Jones")
  .email("bob@example.com")
  .as_admin
  .build

Using Blocks with instance_eval

Passing a block to the builder makes the syntax even cleaner. Instead of chaining methods, you pass a block to the builder and use instance_eval to evaluate it in the context of the builder instance:

class UserBuilder
  def initialize
    @user = User.new
  end

  def first_name(name)
    @user.first_name = name
    self
  end

  def last_name(name)
    @user.last_name = name
    self
  end

  def email(email)
    raise ArgumentError, "Invalid email format" unless email.include?("@")
    @user.email = email
    self
  end

  def role(role)
    @user.role = role
    self
  end

  def build
    raise ArgumentError, "first_name is required" if @user.first_name.nil?
    raise ArgumentError, "email is required" if @user.email.nil?
    @user
  end

  def self.build(&block)
    builder = new
    builder.instance_eval(&block)
    builder.build
  end
end

Now you can construct users with a block:

user = UserBuilder.build do
  first_name "Carol"
  last_name "White"
  email "carol@example.com"
  role "moderator"
end

The instance_eval call executes the block in the context of the builder, so first_name, last_name, and so on are resolved as method calls on the builder. This removes the need for any explicit receiver inside the block.

Note that instance_eval with a block makes self point to the builder inside the block. This means you lose access to self from the calling context. If you need to call methods from the outer scope, pass the block to instance_exec instead and pass any needed context as arguments.

Builder for Nested Structures

One of the most powerful uses of the builder pattern is constructing nested data structures, such as HTML documents. Because each builder method can return self, you can nest calls to build child elements:

class HtmlBuilder
  def initialize(tag)
    @tag = tag
    @children = []
    @attributes = {}
  end

  def add_class(value)
    @attributes["class"] = value
    self
  end

  def text(content)
    @children << content
    self
  end

  def append(&block)
    child_builder = HtmlBuilder.new("div")
    child_builder.instance_eval(&block)
    @children << child_builder.to_html
    self
  end

  def to_html
    attrs = @attributes.map { |k, v| " #{k}=\"#{v}\"" }.join
    if @children.empty?
      "<#{@tag}#{attrs} />"
    else
      "<#{@tag}#{attrs}>#{@children.join}</#{@tag}>"
    end
  end
end

html = HtmlBuilder.new("div").add_class("container").append do
  text "Hello, World!"
end

html.to_html
# => "<div class=\"container\"><div>Hello, World!</div></div>"

This pattern is the basis for popular HTML builder libraries like Nokogiri and Markaby. Each nested element is itself a builder, and the whole structure is built up through composition.

Validating During Construction

A key advantage of builders is that validation can happen at each step, not just at the end. This gives you faster feedback and keeps validation logic close to the data it checks:

class OrderBuilder
  def initialize
    @order = Order.new
  end

  def customer_name(name)
    raise ArgumentError, "Customer name cannot be blank" if name.to_s.strip.empty?
    @order.customer_name = name
    self
  end

  def add_item(product_id, quantity, price)
    raise ArgumentError, "Quantity must be positive" if quantity <= 0
    raise ArgumentError, "Price must be positive" if price < 0
    @order.items << { product_id: product_id, quantity: quantity, price: price }
    self
  end

  def discount(code)
    valid_codes = ["SAVE10", "SAVE20", "WELCOME"]
    raise ArgumentError, "Unknown discount code" unless valid_codes.include?(code)
    @order.discount_code = code
    self
  end

  def build
    raise ArgumentError, "Customer name is required" if @order.customer_name.nil?
    raise ArgumentError, "Order must have at least one item" if @order.items.empty?
    @order.total = @order.items.sum { |i| i[:quantity] * i[:price] }
    @order
  end
end

With this builder, you get an immediate error if you try to add an item with a negative quantity or use an invalid discount code. You never end up with an invalid Order object because the builder rejects bad data as it is provided.

Practical Example: Building an API Response

A common real-world use for the builder pattern is constructing API responses. Instead of manually assembling hashes, you use a builder:

class ApiResponseBuilder
  def initialize
    @response = { meta: {}, data: nil, errors: [] }
  end

  def meta(key, value)
    @response[:meta][key] = value
    self
  end

  def data(payload)
    @response[:data] = payload
    self
  end

  def error(message, code: nil)
    err = { message: message }
    err[:code] = code if code
    @response[:errors] << err
    self
  end

  def paginated(page:, per_page:, total:)
    meta(:page, page)
    meta(:per_page, per_page)
    meta(:total, total)
    self
  end

  def build
    raise ArgumentError, "Response must have data or errors" if @response[:data].nil? && @response[:errors].empty?
    @response
  end
end

Usage:

response = ApiResponseBuilder.new
  .meta(:request_id, "req-123")
  .data({ users: [{ name: "Alice" }, { name: "Bob" }] })
  .paginated(page: 1, per_page: 20, total: 2)
  .build

This keeps API response construction consistent and ensures every response has the expected shape.

When to Use the Builder Pattern

The builder pattern is a good fit when:

  • An object has many optional parameters or configuration steps
  • You want validation at each step rather than only at construction
  • You need to construct different variations of the same object
  • The construction process itself is complex or involves nested objects
  • You want a readable, expressive way to construct objects

For simple objects with a few required fields and no validation, a plain initializer or a factory method is usually sufficient. The builder pattern adds complexity that is only justified when that complexity pays off in readability and safety.

See Also