You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+
## Project
6
+
7
+
Cacheable is a Ruby gem by Splitwise that adds method caching via an AOP (Aspect-Oriented Programming) pattern. Include `Cacheable` in a class, annotate methods with `cacheable :method_name`, and results are automatically cached.
The gem uses **module prepending with dynamic method generation** to intercept and cache method calls.
40
+
41
+
### Core flow
42
+
43
+
1.**`Cacheable`** (`lib/cacheable.rb`) — The module users include. On `included`, it extends the host class with `MethodGenerator` and creates a unique anonymous interceptor module that gets prepended to the class.
44
+
45
+
2.**`MethodGenerator`** (`lib/cacheable/method_generator.rb`) — When `cacheable :method_name` is called, this generates five methods on the interceptor module:
46
+
-`method_name` (override) — dispatcher that routes to `with_cache` or `without_cache` based on the `unless:` condition
47
+
-`method_with_cache` — fetch from cache or compute and store
48
+
-`method_without_cache` — bypass cache, call original
49
+
-`method_key_format` — generate the cache key
50
+
-`clear_method_cache` — invalidate the cache entry
51
+
52
+
3.**`CacheAdapter`** (`lib/cacheable/cache_adapter.rb`) — Protocol for cache backends. Default is `:memory`. Required interface: `fetch(key, options, &block)` and `delete(key)`.
53
+
54
+
4.**`MemoryAdapter`** (`lib/cacheable/cache_adapters/memory_adapter.rb`) — Built-in hash-backed in-memory cache. Production use typically wires in `Rails.cache` or a custom adapter.
55
+
56
+
### Key design details
57
+
58
+
- Each class that includes `Cacheable` gets its own unique interceptor module (created via `Module.new`), which is prepended to the class. This is how `super` chains through to the original method.
59
+
- The `unless:` option accepts a proc/symbol that, when truthy, skips caching and calls the original method directly.
60
+
-`key_format:` accepts a proc receiving `(target, method_name, args, **kwargs)` for custom cache key generation.
Copy file name to clipboardExpand all lines: README.md
+63-40Lines changed: 63 additions & 40 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -82,9 +82,9 @@ end
82
82
> a = GitHubApiAdapter.new
83
83
> a.star_count
84
84
Fetching data from GitHub
85
-
=> 19
85
+
=> 58
86
86
> a.star_count
87
-
=> 19
87
+
=> 58
88
88
89
89
# Notice that "Fetching data from GitHub" was not output the 2nd time the method was invoked.
90
90
# The network call and result parsing would also not be performed again.
@@ -102,12 +102,12 @@ The cache can intentionally be skipped by appending `_without_cache` to the meth
102
102
> a = GitHubApiAdapter.new
103
103
> a.star_count
104
104
Fetching data from GitHub
105
-
=> 19
105
+
=> 58
106
106
> a.star_count_without_cache
107
107
Fetching data from GitHub
108
-
=> 19
108
+
=> 58
109
109
> a.star_count
110
-
=> 19
110
+
=> 58
111
111
```
112
112
113
113
#### Remove the Value via `clear_#{method}_cache`
@@ -118,15 +118,15 @@ The cached value can be cleared at any time by calling `clear_#{your_method_name
118
118
> a = GitHubApiAdapter.new
119
119
> a.star_count
120
120
Fetching data from GitHub
121
-
=> 19
121
+
=> 58
122
122
> a.star_count
123
-
=> 19
123
+
=> 58
124
124
125
125
> a.clear_star_count_cache
126
126
=> true
127
127
> a.star_count
128
128
Fetching data from GitHub
129
-
=> 19
129
+
=> 58
130
130
```
131
131
132
132
## Additional Configuration
@@ -135,7 +135,7 @@ Fetching data from GitHub
135
135
136
136
#### Default
137
137
138
-
By default, Cacheable will construct a key in the format `[cache_key || class_name, method_name]` without using method arguments.
138
+
By default, Cacheable will construct a key in the format `[cache_key || class_name, method_name]` without using method arguments. If a cached method is called with arguments while using the default key format, Cacheable will emit a warning to stderr since different arguments will return the same cached value. To silence the warning, provide a `:key_format` proc that includes the arguments in the cache key.
139
139
140
140
If the object responds to `cache_key` its return value will be the first element in the array. `ActiveRecord` provides [`cache_key`](https://api.rubyonrails.org/classes/ActiveRecord/Integration.html#method-i-cache_key) but it can be added to any Ruby object or overwritten. If the object does not respond to it, the name of the class will be used instead. The second element will be the name of the method as a symbol.
141
141
@@ -155,12 +155,13 @@ require 'net/http'
155
155
classGitHubApiAdapter
156
156
includeCacheable
157
157
158
-
cacheable :star_count, key_format:->(target, method_name, method_args) do
*`target` is the object the method is being called on (`#<GitHubApiAdapter:0x0…0>`)
172
173
*`method_name` is the name of the method being cached (`:star_count`)
173
-
*`method_args` is an array of arguments being passed to the method (`[params]`)
174
+
*`method_args` is an array of positional arguments being passed to the method (`[params]`)
175
+
*`**kwargs` are the keyword arguments being passed to the method
174
176
175
177
Including the method argument(s) allows you to cache different calls to the same method. Without the arguments in the cache key, a call to `star_count('cacheable')` would populate the cache and `star_count('tokenautocomplete')` would return the number of stars for Cacheable instead of what you want.
176
178
177
-
In addition, we're including the current date in the cache key so calling this method tomorrow will return an updated value.
179
+
**Note:** The `key_format` proc only receives keyword arguments that the caller explicitly passes — method defaults are not included. That's why the proc uses `kwargs.fetch(:date, Time.now.strftime('%Y-%m-%d'))` to compute its own default when `date:` is omitted. This ensures the cache key always varies by date.
178
180
179
181
```irb
180
182
> a = GitHubApiAdapter.new
181
183
> a.star_count('cacheable')
182
-
Fetching data from GitHub for cacheable
183
-
=> 19
184
+
Fetching data from GitHub for cacheable (as of 2026-02-26)
185
+
=> 58
184
186
> a.star_count('cacheable')
185
-
=> 19
187
+
=> 58
186
188
> a.star_count('tokenautocomplete')
187
-
Fetching data from GitHub for tokenautocomplete
188
-
=> 1164
189
+
Fetching data from GitHub for tokenautocomplete (as of 2026-02-26)
190
+
=> 1309
189
191
> a.star_count('tokenautocomplete')
190
-
=> 1164
192
+
=> 1309
191
193
192
194
# In this example the follow cache keys are generated:
You can control if a method should be cached by supplying a proc to the `unless:` option which will get the same arguments as `key_format:`. This logic can be defined in a method on the class and the name of the method as a symbol can be passed as well. **Note**: When using a symbol, the first argument, `target`, will not be passed but will be available as `self`.
201
+
You can control if a method should be cached by supplying a proc to the `unless:` option which will get the same arguments as `key_format:` (`target, method_name, method_args, **kwargs`). This logic can be defined in a method on the class and the name of the method as a symbol can be passed as well. **Note**: When using a symbol, the first argument, `target`, will not be passed but will be available as `self`.
200
202
201
203
```ruby
202
204
# From examples/conditional_example.rb
@@ -208,18 +210,19 @@ require 'net/http'
208
210
classGitHubApiAdapter
209
211
includeCacheable
210
212
211
-
cacheable :star_count, unless::growing_fast?, key_format:->(target, method_name, method_args) do
By default, all classes use the global adapter set via `Cacheable.cache_adapter`. If you need a specific class to use a different cache backend, you can set one directly on the class:
260
+
261
+
```ruby
262
+
classFrequentlyAccessedModel
263
+
includeCacheable
264
+
265
+
self.cache_adapter =MyFasterCache.new
266
+
267
+
cacheable :expensive_lookup
268
+
269
+
defexpensive_lookup
270
+
# ...
271
+
end
272
+
end
273
+
```
274
+
275
+
The class-level adapter takes precedence over the global adapter. Classes without their own adapter fall back to `Cacheable.cache_adapter` as usual.
276
+
254
277
### Flexible Options
255
278
256
279
You can use the same options with multiple cache methods or limit them only to specific methods:
0 commit comments