Ruby Struct: How I Stopped Fighting Raw Data Structures
Stop passing raw arrays around your Ruby code. Use Struct.
My colleague spent two hours debugging order calculations that should have been straightforward.
You know that moment when you realize a bug exists because your code is accessing orders[2]
in one place and orders[1]
in another, and nobody knows what these indices actually mean? This was his.
In this issue, I’ll take you through how Ruby Struct transforms confusing raw data into meaningful, self-documenting code. No theory dumps – just real code, real problems, and real solutions.
🚀 What you’ll learn:
How to wrap meaningless data structures in business-friendly Struct classes
Why soft typing prevents bugs that primitive types can’t catch
When to choose Struct over full classes for domain modeling
How proper data encapsulation creates ubiquitous language across teams
The Problem
We had painful data confusion throughout our Ruby codebase:
# What does this actually represent?
orders = Order.all
user_data = [user.name, user.email, user.created_at]
coordinates = [lat, lng, elevation]
# Which index is what? Good luck remembering.
def calculate_shipping(orders)
orders.each do |order|
# Is this the right array position?
if order[3] > threshold
# What is order[3]? Weight? Price? Quantity?
apply_surcharge(order)
end
end
end
💡 Warning Signs:
Raw arrays where meaning isn’t clear from context
Magic indices scattered throughout the codebase
String/Integer types that don’t explain domain meaning
Debugging sessions that involve guessing what data represents
The Journey: From Problem to Solution
Step 1: Wrap Raw Data in Meaningful Structs
Before:
# Poor: No context, just raw data
orders = Order.all
user_coordinates = [user.lat, user.lng]
After:
# Better: Wrapped in meaningful structures
Orders = Struct.new(:orders, keyword_init: true) do
def self.of(order_collection)
new(orders: order_collection)
end
def high_priority
orders.select(&:urgent?)
end
end
Coordinates = Struct.new(:latitude, :longitude, keyword_init: true)
orders = Orders.of(Order.all)
location = Coordinates.new(latitude: user.lat, longitude: user.lng)
🎯 Impact:
Code became self-documenting through meaningful names
Index-based bugs disappeared completely
Business logic could be added directly to data structures
Step 2: Create Soft Typing Through Named Attributes
Instead of relying only on primitive types, we gave meaning to values by naming them:
# Before: What kind of string is this?
def process_user_info(name, email, role)
# name, email, role are all just strings
# No way to enforce their meaning
end
# After: Struct enforces meaning through names
UserInfo = Struct.new(:name, :email, :role, keyword_init: true) do
def admin?
role == 'admin'
end
def valid_email?
email.include?('@')
end
end
def process_user_info(user_info)
# user_info.name is clearly a name
# user_info.email is clearly an email
# Business logic is encapsulated
end
The Aha Moment
The real breakthrough came when we realized that object-oriented design focuses on the messages objects send to each other.
Struct wasn’t just about organizing data – it was about creating meaningful interfaces that hide implementation details while exposing business intent.
Real Numbers From This Experience
Before: 15+ index-based bugs per month
After: Zero index-based bugs
Before: 30 minutes average debugging time for data-related issues
After: 5 minutes average debugging time
Code readability: 80% improvement in code review feedback
The Final Result
# Clean, self-documenting, business-focused code
Orders = Struct.new(:orders, keyword_init: true) do
def self.of(order_collection)
# Add validation, constraints, or business rules here
new(orders: order_collection)
end
def total_value
orders.sum(&:amount)
end
def urgent_orders
orders.select(&:urgent?)
end
# Hide internal structure - consumers can't depend on array order
def each(&block)
orders.each(&block)
end
end
# Usage becomes expressive and safe
customer_orders = Orders.of(Order.where(customer_id: current_user.id))
urgent_total = customer_orders.urgent_orders.sum(&:amount)
🎉 Key Improvements:
Business terminology directly in the code
Encapsulated behavior with data
Protection against implementation detail leakage
Foundation for ubiquitous language across the team
Monday Morning Action Items
Quick Wins (5-Minute Changes)
Find one raw array in your codebase and wrap it in a Struct
Replace magic indices with named Struct attributes
Add one business method to an existing data structure
Next Steps
Audit your codebase for primitive obsession patterns
Create Struct wrappers for your most-used data collections
Document the business meaning of your data structures
Your Turn!
The Struct Refactoring Challenge
How would you refactor this confusing data structure?
# Current: What do these represent?
user_stats = [
user.login_count,
user.last_login,
user.subscription_type,
user.is_premium?
]
def send_marketing_email(stats)
if stats[0] > 10 && stats[3]
# What logic is this actually implementing?
PremiumMailer.send_retention(stats[1])
end
end
💬 Discussion Prompts:
How would you name the Struct to reflect business intent?
What business methods would you add to encapsulate the logic?
How could this improve team communication about user engagement?
🔧 Useful Resources:
Found this useful? Share it with a fellow developer who’s tired of debugging magic indices! And don’t forget to subscribe for more practical Ruby insights.
Happy coding! 🚀
💡 Tips and Notes
Pro Tip: Use
keyword_init: true
for better readability and to avoid positional argument bugsRemember: Struct is perfect when you need more than a Hash but less than a full class