Ruby
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Methods and Blocks
-
Section 4: Object-Oriented Ruby
-
Section 5: Metaprogramming
-
Section 6: Working with Data and Files
-
Section 7: Ruby Frameworks Overview
-
Section 8: Ecosystem and Testing
-
Section 9: Practical Projects
-
Section 10: Interview Practice
-
Section 11: Data Structures and Algorithms in Ruby
-
Section 12: More Practice Exercises
-
Section 13: Enumerable and Functional Style
-
Section 14: More OOP Practice
-
Section 15: More Testing
-
Section 16: Enumerable and Comparable Modules In Depth
-
Section 17: Ruby Standard Library: Core Utilities
-
Section 18: Ruby Standard Library: Data and Security
-
Section 19: Ruby Standard Library: CLI and Text
-
Section 20: Ruby Networking
-
Section 21: Ruby on Rails Deep Dive
-
Section 22: Ruby Metaprogramming Deep Dive
-
Section 23: Ruby Design Patterns
-
Section 24: Ruby Concurrency
-
Section 25: Ruby Testing Deep Dive
-
Section 26: Ruby Gems and Packaging
-
Section 27: Ruby Performance
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Rails API Development
-
Section 31: Rails Authentication and Authorization
-
Section 32: Rails Testing Deep Dive
-
Section 33: Rails Performance
-
Section 34: Rails Deployment
-
Section 35: Sinatra and Lightweight Ruby Web Apps
-
Section 36: More Ruby Language Deep Dive
-
Section 37: Ruby 3.x Modern Features
-
Section 38: More Data Structures in Ruby
-
Section 39: More Practical Projects
-
Section 40: Ruby Ecosystem Tools
-
Section 41: More Practice and Review
-
Section 42: Final Practice and Mastery
-
Section 43: Ruby Interview Deep Dive
-
Section 44: Ruby Background Processing Deep Dive
-
Section 45: Ruby GraphQL
-
Section 46: Ruby Object Model Deep Dive
-
Section 47: Ruby Hanami Framework Overview
-
Section 48: Ruby gRPC and Protocol Buffers
-
Section 49: Ruby Data Processing
-
Section 50: Ruby Search Integration
-
Section 51: Ruby File Upload and Media
-
Section 52: Ruby Email and Notifications
-
Section 53: Ruby Admin Panels
-
Section 54: Ruby Feature Flags and Experimentation
-
Section 55: Ruby Monitoring and Observability
-
Section 56: Ruby Docker and Deployment Deep Dive
-
Section 57: Ruby Security Deep Dive
-
Section 58: More Advanced Metaprogramming
-
Section 59: More Final Projects
146: Frozen String Literals and Immutability
One of the quirks of Ruby that often catches developers off guard is how it handles strings. By default, every time you write a string literal—like "active" or "pending"—Ruby allocates a brand new object in memory. It sounds trivial, but in a large application where you're checking statuses or keys thousands of times a second, those allocations add up. You're essentially creating a mountain of tiny, identical objects that the Garbage Collector then has to spend time cleaning up.
The invisible cost of repeated literals
I want you to imagine you're building a permissions system. You might have a method that checks a user's role against a set of allowed roles. In a naive implementation, it looks like this:
def authorized?(user_role)
# Every time this method runs, Ruby creates a new string object for "admin"
if user_role == "admin"
true
else
false
end
end
On the surface, this is perfectly fine. But if this method is called inside a loop processing 10,000 users, you've just created 10,000 separate string objects that all contain the exact same characters. If you check the object_id of "admin" twice in the same method, you'll see they are different. It's a waste of memory, and while modern Ruby is fast, this is the kind of "death by a thousand cuts" that slows down a production environment.
Locking things down with the magic comment
To fix this, we use a "magic comment" at the very top of the file: # frozen_string_literal: true. When you add this, you're telling Ruby, "Every string literal in this file should be frozen." Instead of creating a new object every time, Ruby creates one single object and reuses it everywhere.
# frozen_string_literal: true
def authorized?(user_role)
# Now, "admin" is a single, immutable object reused across every call
user_role == "admin"
end
This is a massive win for performance. You're reducing the pressure on the Garbage Collector and making your code more predictable. I've made it a habit to put this at the top of every single file I write. It's effectively the "modern" way to write Ruby, and it's why you'll see it in almost every professional gem or Rails project today.
When the freeze bites back
There is a catch, though. Since the strings are now immutable, you can't change them. If you try to modify a frozen string, Ruby will throw a FrozenError. This is where the "better way" can feel like it's breaking your code if you aren't careful.
Consider a scenario where you want to normalize a status string by capitalizing it. If you've frozen your literals, this will crash:
# frozen_string_literal: true
status = "pending"
status << "..." # This will raise FrozenError: can't modify frozen String
The mistake here is using a mutating method (like << or upcase!) on a literal. When you actually need a mutable copy of a frozen string, you just have to be explicit about it. I usually do this by calling .dup or using a method that returns a new string instead of modifying the original in place.
# frozen_string_literal: true
status = "pending"
# .dup creates a fresh, unfrozen copy of the frozen literal
mutable_status = status.dup
mutable_status << "..." # This works perfectly!
The trade-off is simple: you trade a tiny bit of convenience (the ability to mutate any string anywhere) for a significant gain in memory efficiency and safety. It forces you to think about where your data is changing, which generally leads to cleaner, less buggy code.
📋 Practical Task
Refactoring the Permission Guard for Memory Efficiency
You have been handed a legacy file that handles API request validation. It is currently creating thousands of unnecessary string objects and contains a bug where a string is being mutated in place, which will cause a crash once we optimize the file.
Your task:
- Add the
# frozen_string_literal: truemagic comment to the top of the code. - Identify the line where the string is being mutated using
gsub!and fix it so that it creates a new string (or uses.dup) instead of modifying the frozen literal. - Ensure the
validate_tokenmethod still returns the correct modified string.
# TODO: Add magic comment here
def validate_token(token)
# This literal is created every time the method is called
prefix = "TOKEN_"
# This line will crash once the magic comment is added
# because it attempts to mutate the frozen "TOKEN_" string.
prefix.gsub!("TOKEN", "AUTH")
return prefix + token
end
puts validate_token("12345") # Expected output: AUTH_12345
There are no comments for now.