Memoize: A Ruby Trick to Avoid Repeated Expensive Calculations
Real‑World Scenario
Not long ago I was building a service that aggregates weather data for a small town. Each request required pulling raw data from an external API, parsing daily records, and then computing the average temperature for the past week. The API was reliable but noisy, so I implemented a simple retry loop and a few transformations. What I noticed after a few busy evenings was that the same calculations were being performed over and over again—once per request, once per background job, and even in a background thread that refreshed the cache every hour. The repeated network calls and data processing started to dominate the runtime and inflate the cost of the service.
In Ruby, the idiom that solves this pattern is memoization. It lets you tag a method as "expensive to compute, cheap to reuse" and automatically cache its result after the first call. The result is cleaner code, better performance, and fewer accidental duplicate network requests. Below is how I introduced memoization into the weather service and why it turned out to be a better fit than a manual `@var ||= …` pattern.
Using `memoize` in Practice
Ruby ships with ActiveSupport::Memoizable (included via `ActiveSupport`), which provides the `memoize` method. You simply decorate the method you want cached, and the framework stores the result on the instance. The first call triggers the original computation; subsequent calls retrieve the stored value directly.
Here is the refactored `WeatherService`. I kept the external API call separate for clarity, but the key is the `memoize :average_weekly_temperature` line.
class WeatherService
# Expose the public API
def self.for(city, api_key)
new(city, api_key)
end
def initialize(city, api_key)
@city = city
@api_key = api_key
# No need to manage a separate cache variable
end
# This method is now memoized – expensive work runs once per instance
memoize :average_weekly_temperature
# Compute the weekly average by fetching raw data and averaging the temps
def average_weekly_temperature
raw = fetch_raw_data
temps = raw.map { |day| day[:temp] }
# Ruby's Enumerable#sum works on floats; guard against empty arrays
return 0.0 if temps.empty?
temps.sum / temps.size
end
private
# Simulate a network call – in reality this would be an HTTP request
def fetch_raw_data
# Example response shape
[
{ date: '2024-09-01', temp: 18.4 },
{ date: '2024-09-02', temp: 19.1 },
{ date: '2024-09-03', temp: 17.8 },
{ date: '2024-09-04', temp: 20.0 },
{ date: '2024-09-05', temp: 21.3 },
{ date: '2024-09-06', temp: 19.9 },
{ date: '2024-09-07', temp: 18.7 }
]
end
end
Notice that there is no `@average ||= …` pattern. The `memoize` macro does the caching internally, and the method can be called multiple times without re‑invoking `fetch_raw_data`. The caching is transparent to the caller, which makes the code easier to reason about.
Why Choose Memoize Over Manual Caching
Manual caching works, but it adds noise to the class. You have to remember to prefix every cached method with `||=` and keep the cache variable in sync with the method’s lifecycle. That extra boilerplate can lead to bugs, especially when the method is called from multiple threads or when the instance is serialized (e.g., in a Rails background job).
- Clarity: The intent is explicit – `memoize :average_weekly_temperature` reads like documentation.
- Thread safety: ActiveSupport’s memoization uses a simple `@cache` hash and guards against concurrent writes, which is safer than a naive `||=` in a multithreaded environment.
- Serialization: The cached value is stored on the instance and can be bypassed if the object is frozen or duplicated, avoiding stale data.
- Maintainability: Adding new cached methods is as simple as sprinkling `memoize` lines; there is no need to edit multiple files or introduce a separate cache service.
In a Rails context, memoization also works nicely with the request‑lifecycle. If you create a service object per request, the cache is limited to that request anyway, which is exactly what you want for per‑request computations.
Pro tip: When you start seeing the same expensive computation appear in multiple methods of a class, consider memoizing the helper that does the work and calling it from each method. This reduces duplication and keeps a single source of truth for the cached result.
There are edge cases where memoization can be counterproductive. If the result depends on mutable state that changes after the first call, you might accidentally return stale data. In such cases, you can either avoid memoizing or implement a manual invalidation strategy. The key is to understand the lifecycle of the data you are caching.
When to Apply and When to Skip
Use `memoize` when:
- The method is pure – same inputs always produce the same output.
- Computing the result is relatively expensive (network I/O, DB queries, heavy math).
- You expect the result to be needed multiple times within the same object’s lifetime.
- You can tolerate the memory overhead of storing the result per instance.
Avoid memoizing when:
- The method reads external mutable state that can change (e.g., a user’s session flag).
- Instances are long‑lived and the cached value becomes stale over time without a clear invalidation path.
- You are working in a highly concurrent environment where you need fine‑grained control over cache keys (consider `ActiveSupport::Cache` instead).
In the weather service, the raw data never changes during the lifetime of the instance, so memoization is a perfect fit. If the service were polling a live feed that updates every minute, I would instead use a timed cache or a background job that refreshes the stored value.
Wrap Up
Memoization is a small but powerful pattern that Ruby makes almost effortless to adopt via `ActiveSupport::Memoizable`. By tagging expensive methods with `memoize`, you keep your code clean, avoid duplicate work, and sidestep many of the pitfalls of manual caching. The weather example shows how a few lines of markup can turn a repetitive, network‑heavy service into a lean, responsive component. When you recognize a calculation that repeats, give memoization a try – you’ll likely find it becomes a go‑to tool in your everyday toolbox.