Skip to Content
Course content

146: Frozen String Literals and Immutability

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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: true magic 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_token method 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
Rating
0 0

There are no comments for now.

to be the first to leave a comment.