Reason #62 • March 3rd, 2026

Struct

Back when writing about easy getters and setters I mentioned that I don't always write classes....

So what do I do then?

Well, whenever an object doesn't need sophisticated state management, there are often simpler alternatives to writing a class. Simple data structures like Arrays and Hashes often do the job just fine. But when you need the next step up, Struct is a great option:

Ruby
Person = Struct.new(:name, :birth_date)

# We can instantiate a person using ordered arguments:
person = Person.new("Alice", Time.parse("1990-01-01"))
# => #<struct Person name="Alice", birth_date=#<Date: 1990-01-01>>

# Or we can use keyword arguments:
person = Person.new(birth_date: Date.parse("1985-05-15"), name: "Bob")
# => #<struct Person name="Bob", birth_date=#<Date: 1985-05-15>>

# We can access attributes using dot notation:
person.name # => "Bob"
person.birth_date # => #<Date: 1985-05-15>

# Attributes are mutable by default:
person.name = "Charlie"
person.name # => "Charlie"

# Destructuring assignment is also supported:
person => { name: }
name # => "Charlie"
      
JavaScript
class Person {
  constructor(name, birthDate) {
    this.name = name;
    this.birthDate = birthDate;
  }
}

// The Person class we created can only be instantiated using ordered arguments:
const person = new Person("Alice", new Date("1990-01-01"));
// => Person { name: 'Alice', birthDate: Mon Jan 01 1990 01:00:00 UTC }

// We can access attributes using dot notation:
person.name; // => "Alice"
person.birthDate; // => Mon Jan 01 1990 01:00:00 UTC

// Attributes are mutable:
person.name = "Bob";
person.name; // => "Bob"

// Destructuring assignment is also supported:
const { name } = person;
name; // => "Bob"
      

What if we want to define methods on our Person struct? We can do that by passing a block to Struct.new:

Ruby
Person = Struct.new(:name, :birth_date) do
  def age
    today = Date.today
    age = today.year - birth_date.year
    age -= 1 if today.yday < birth_date.yday
    age
  end
end

person = Person.new("Alice", Date.parse("1990-01-01"))
person.age # => 36 (as of the time of writing)
      
JavaScript
class Person {
  constructor(name, birthDate) {
    this.name = name;
    this.birthDate = birthDate;
  }

  get age() {
    const now = new Date()
    const bd = this.birthDate;
    const hadBirthday =
      now.getMonth() > bd.getMonth() ||
      (now.getMonth() === bd.getMonth() &&
        now.getDate() >= bd.getDate());

    let age = now.getFullYear() - bd.getFullYear();
    if (!hadBirthday) age -= 1;
    return age;
  }
}

const person = new Person("Alice", new Date("1990-01-01"));
person.age; // => 36 (as of the time of writing)
      

Ruby collects some extra style points courtesy of Date#yday here, making the age calculation a little more concise. Just don't show this code to anyone born on February 29th!

History

The Struct class has been part of Ruby since its inception. However, it has undergone quite a few changes over the years.

The initial implementation of Struct required a leading String argument to name the struct, and its documentation assigned it to a regular variable rather than a constant, e.g. person = Struct.new("Person", :name, :birth_date). It was only possible to instantiate the struct using ordered arguments.

In Ruby 1.6 the leading String argument was made optional and the Ruby parser learnt how to infer the struct's name from the constant it was being assigned to.

In Ruby 2.5, the ability to instantiate a struct using keyword arguments was added, but it required an explicit keyword_init: true option to become enabled, e.g. Person = Struct.new(:name, :birth_date, keyword_init: true).

In Ruby 2.7, destructuring assignment support was added.

In Ruby 3.2, the keyword_init: true option was made the default, so now all structs support keyword initialization without needing to specify any options.

Reason #63 ?