Struct — Lightweight Data Objects in Ruby

· 4 min read · Updated March 16, 2026 · beginner
ruby struct data-structures guide

Ruby’s Struct class gives you a quick way to bundle related data together without writing a full class. Think of it as a class with predetermined attributes — perfect for data Transfer Objects, configuration objects, or any place where you need a simple container for a few related values.

Creating a Struct

The simplest way to create a struct is with Struct.new, passing in attribute names as symbols or strings:

Point = Struct.new(:x, :y)
point = Point.new(10, 20)

point.x  # => 10
point.y  # => 20

You can also create an anonymous struct and use it directly:

person = Struct.new(:name, :age).new("Alice", 30)
person.name  # => "Alice"

Struct as a Class

When you assign a struct to a constant (like Point), it becomes a reusable class:

Rectangle = Struct.new(:width, :height)

rect1 = Rectangle.new(100, 50)
rect2 = Rectangle.new(200, 100)

rect1.width  # => 100

You can call methods on struct instances just like regular Ruby objects:

point = Point.new(3, 4)
point.to_h  # => {:x=>3, :y=>4}
point.values  # => [3, 4]

Keyword Arguments

By default, structs use positional arguments. You can switch to keyword arguments with keyword_init: true:

Config = Struct.new(:host, :port, :ssl, keyword_init: true)

config = Config.new(host: "localhost", port: 443, ssl: true)
config.host  # => "localhost"
config.port  # => 443

This makes calls more explicit and less error-prone when you have many attributes:

# Positional — easy to mix up order
User.new("john", 25, "john@example.com")

# Keyword — self-documenting
User.new(name: "john", age: 25, email: "john@example.com")

Adding Methods to Structs

Structs support custom methods right in the definition:

Product = Struct.new(:name, :price) do
  def formatted_price
    "$%.2f" % price
  end
  
  def discounted_price(discount)
    price * (1 - discount)
  end
end

product = Product.new("Widget", 29.99)
product.formatted_price  # => "$29.99"
product.discounted_price(0.1)  # => 26.991

This makes structs incredibly flexible — they’re lightweight classes that still support full Ruby object behavior.

Default Values

You can provide default values for attributes:

Server = Struct.new(:host, :port, :protocol) do
  def initialize(host = "localhost", port = 80, protocol = "http")
    super
  end
end

Server.new  # => #<struct Server host="localhost", port=80, protocol="http">

Or use a block to define defaults more elegantly:

Options = Struct.new(:debug, :verbose) do
  def initialize(*)
    super
    self.debug ||= false
    self.verbose ||= false
  end
end

Ruby 3.2+ also supports default and default_proc for hash-like defaults.

Struct vs OpenStruct

Ruby provides two similar mechanisms for quick data objects:

FeatureStructOpenStruct
PerformanceFaster (fixed layout)Slower (hash-backed)
AttributesDefined at creationAddable at runtime
Keyword argsSupportedSupported (Ruby 3.0+)
MemoryMore efficientMore memory

OpenStruct is slower because it uses a hash internally. Use Struct when you know the attributes ahead of time:

# Struct — faster, fixed schema
Point = Struct.new(:x, :y)

# OpenStruct — flexible, slower
point = OpenStruct.new(x: 1, y: 2)
point.z = 3  # can add attributes freely

Practical Examples

Configuration Object

AppConfig = Struct.new(:env, :debug, :database_url, keyword_init: true)

config = AppConfig.new(
  env: "production",
  debug: false,
  database_url: "postgresql://localhost/app"
)

Data Transfer Object

UserDTO = Struct.new(:id, :name, :email, :created_at, keyword_init: true)

def build_user(data)
  UserDTO.new(
    id: data["id"],
    name: data["name"],
    email: data["email"],
    created_at: Time.parse(data["created_at"])
  )
end

Multiple Instances

Card = Struct.new(:rank, :suit)

deck = Card.new("A", "♠️")
deck.rank  # => "A"
deck.suit  # => "♠️"

Enumerating Struct Members

You can iterate over struct members programmatically:

Config = Struct.new(:host, :port, :ssl)

config = Config.new("example.com", 443, true)

config.members  # => [:host, :port, :ssl]

config.each_pair do |name, value|
  puts "#{name}: #{value}"
end
# host: example.com
# port: 443
# ssl: true

When to Use Structs

Structs shine in these scenarios:

  • Configuration objects — group related settings
  • Return values — multiple values from a method
  • Data transfer — simple DTOs between layers
  • Immutable data — structs work well with frozen objects

Avoid structs when you need complex validation, inheritance, or behavior-heavy objects. In those cases, write a full class.

See Also