My decorator-style memoization shard for Crystal

Concise, argument-aware method caching without the boilerplate.

August 23, 2026 · David Runger

A fox memoizing stuff

I was surprised that I couldn’t find an established Crystal shard for memoization with the API I wanted. Rubyists have plenty of options, including MemoWise, Memoist, and the memoization support in dry-core. In Crystal, my searches mostly turned up snippets or code embedded in a larger framework.

So I made memoization, a small Crystal shard that lets me write this:

memoize def current_content_sha(file_or_directory : String) : String
  # Calculate and return a SHA-256 digest...
end

Each distinct argument gets its own cached result, and repeated calls reuse it.

Why I wanted a memoization shard

Memoization is an easy optimization to understand: save a method’s result and reuse it when the same arguments are passed again.

For a method without arguments, I could manage that myself with an instance variable:

def expensive_value : String
  @expensive_value ||= calculate_expensive_value
end

However, that adds state-management boilerplate to a method whose meaningful job is simply to calculate a value. It also requires more careful handling when nil or false is valid; otherwise, the calculation can run again on every call.

Arguments make manual memoization more cumbersome. Remembering current_content_sha("README.md") independently from current_content_sha("src") requires a hash keyed by the method’s arguments, plus lookup and assignment logic around the actual implementation.

One reason I enjoy Ruby and Crystal is that they strip away boilerplate and leave expressive code. Building and maintaining a cache by hand felt like a step backward. I wanted the method definition to say what it calculates, with memoize as the one clear declaration that its results should be reused.

The code I found

My path to the shard included a Crystal Forum thread about a caching decorator. Someone pointed to the memoize macro in the Lucky framework, whose syntax was almost exactly what I wanted:

memoize def somefun(a, b) : Whatever
  # ...
end

There was one important limitation: for methods with arguments, a call with different arguments replaced the previous cached value. The forum thread’s original poster described the same problem:

I’m looking to use it for dynamic programming, so I’d want to use it for all param sets, not just the latest.

I adapted Lucky’s code so that methods with arguments use a hash keyed by argument tuples, preserving results for many sets of arguments instead of only the latest one.

I was very inexperienced with Crystal macros, and getting this working was difficult and fairly bumbling. The macro has to inspect the method definition at compile time and generate a typed cache, an uncached implementation, and a public wrapper. Somewhat to my surprise, I figured it out.

The code first lived in my dotfiles. Git history tells me that I added it in August 2024 while converting a personal tool from Ruby to Crystal. In March 2025, I extracted it into the standalone memoization shard so that I could reuse it elsewhere.

Installation and use

Add the shard to shard.yml:

dependencies:
  memoization:
    github: davidrunger/memoization

Then install it and require it:

shards install
require "memoization"

Add memoize before a method definition. Both the return type and every argument type must be explicit so that the macro can construct the correctly typed cache:

class Greeting
  memoize def message(name : String) : String
    puts "Generating a greeting for #{name}"
    "Hello, #{name}!"
  end
end

greeting = Greeting.new

greeting.message("Jane")
# Prints "Generating a greeting for Jane" and returns "Hello, Jane!"

greeting.message("John")
# Prints "Generating a greeting for John" and returns "Hello, John!"

greeting.message("Jane")
# Returns the cached "Hello, Jane!" without printing anything

The cache belongs to the object instance, with a separate entry for each argument combination. The shard also supports methods without arguments, nil results, default and named arguments, private methods, and method names ending in ? or !.

Where I use it

I use the shard throughout the Crystal tools in my dotfiles. For example, one method calculates a SHA-256 digest for a file or directory. Calls for the same path reuse that work while other paths are cached independently.

I also use it in Skedjewel, a scheduler for Sidekiq jobs. Skedjewel memoizes parsed schedule values and its lock manager. I use Skedjewel to run scheduled jobs for the david_runger Rails app, so this is “production” use in the sense that it runs my hobby app, not evidence from a large commercial deployment.

Limitations

This is a small library that solves the problem I have, not a comprehensive caching system. In particular:

A more sophisticated implementation might address these limitations. I haven’t needed those features, so I have kept the shard simple.

If that tradeoff fits your program, you can find the source, installation instructions, and complete examples at github.com/davidrunger/memoization.

This blog is open source. Improve this post.