Skip to content
AgileLogics
5 min read

Struct and Data

Prerequisites: Classes and objects

Many classes are just a few named values bundled together. Ruby has two shortcuts for building them without the boilerplate.

Struct — a mutable bundle of fields

Point = Struct.new(:x, :y) do
  def distance_from_origin = Math.sqrt(x**2 + y**2)
end

pt = Point.new(3, 4)
p pt.x
p pt.distance_from_origin
pt.x = 6
p pt
p pt == Point.new(6, 4)
3
5.0
#<struct Point x=6, y=4>
true

Struct.new generates a class with a constructor, readers, writers, == based on the values, and a readable inspect. The optional block adds your own methods.

Data — an immutable value object (Ruby 3.2+)

Data is like Struct, but its values can't be changed after creation. That makes it the better default for value objects — things defined entirely by their values, such as money, coordinates, or date ranges:

Money = Data.define(:amount, :currency) do
  def to_s = format("%.2f %s", amount, currency)

  def +(other)
    raise ArgumentError, "currency mismatch" unless currency == other.currency

    with(amount: amount + other.amount)
  end
end

price = Money.new(amount: 10, currency: "EUR")
tax = Money.new(2.5, "EUR")
puts price + tax
p price
price.instance_variable_set(:@amount, 99) rescue puts "can't change a Data object"
12.50 EUR
#<data Money amount=10, currency="EUR">
can't change a Data object
  • Data.define objects can be created with keyword or positional arguments.
  • There are no setters. with(...) returns a copy with some values changed.
  • Missing an argument raises an error, unlike Struct, which quietly fills in nil.

When to use which

  • Data — small immutable values. Prefer it by default.
  • Struct — when you genuinely need to change fields in place.
  • A full class — when the object has real behavior, validation, or private state.

Common mistakes

  • Using a plain hash ({ x: 3, y: 4 }) for data passed all over a program. A typo in a hash key gives a silent nil; a typo in a Data field raises NoMethodError straight away.