0.0
The project is in a healthy, maintained state
TypedCache is a Ruby caching library designed to eliminate common caching pitfalls by providing a monadic, type-safe API that makes cache operations explicit and predictable. Cache interactions are first-class operations with comprehensive error handling and transparent state management. The library supports wrapping other caching libraries via custom backends and ActiveSupport::Cache is supported out of the box.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
 Dependencies
 Project Readme

TypedCache

Gem Version GitHub Release Date GitHub last commit

GitHub License

TypedCache is a lightweight, type-safe façade around your favourite Ruby cache stores. It adds three things on top of the raw back-end implementation:

  1. Namespacing – hierarchical Namespace helpers prevent key collisions. You can create nested namespaces easily, like Namespace.at("users", "profiles", "avatars").
  2. Stronger types – RBS signatures as well as monadic types like Either, Maybe, and Snapshot wrap cache results so you always know whether you have a value, an error, or a cache-miss.
  3. Composable decorators – behaviours like instrumentation can be layered on without touching the underlying store.

TL;DR – Think Faraday or Rack middlewares, but for caching.


Installation

bundle add typed_cache && bundle install
# or
gem install typed_cache

This gem does is also cryptographically signed, if you want to ensure the gem was not tampered with, make sure to use these commands:

bundle add typed_cache && bundle install --trust-policy=HighSecurity
# or
gem install typed_cache -P HighSecurity

If there are issues with unsigned gems, use MediumSecurity instead.

Quick start

require "typed_cache"

# 1. Build a store
store = TypedCache.builder
  .with_backend(:memory, shared: true)
  .with_instrumentation(:rails) # e.g. using ActiveSupport
  .build
  .value # unwrap Either for brevity

# 2. Get a reference to a key
user_ref = store.ref("users:123") # => CacheRef

# 3. Fetch and compute if absent
user_snapshot = user_ref.fetch do
  puts "Cache miss! Computing..."
  { id: 123, name: "Jane" }
end.value # => Snapshot

puts "Found: #{user_snapshot.value} (from_cache?=#{user_snapshot.from_cache?})"

Builder API

Step Purpose
with_backend(:name, **opts) Mandatory. Configure the concrete Backend and its options.
with_decorator(:key) Optional. Add a decorator by registry key.
with_instrumentation(:source) Optional. Add instrumentation, e.g. :rails or :dry.
build Returns Either[Error, Store].

Back-ends vs Decorators

  • Back-end (TypedCache::Backend) – persists data (Memory, Redis, etc.).
  • Decorator (TypedCache::Decorator) – wraps an existing store to add behaviour (Instrumentation, Logging, Circuit-Breaker …).

Both include the same public Store interface, so they can be composed freely. Registries keep them separate:

TypedCache::Backends.available   # => [:memory, :active_support]
TypedCache::Decorators.available # => [:instrumented]

Register your own

class RedisBackend
  include TypedCache::Backend
  # … implement #get, #set, etc.
end

TypedCache::Backends.register(:redis, RedisBackend)
class LogDecorator
  include TypedCache::Decorator
  def initialize(store) = @store = store
  def set(key, value)
    puts "[cache] SET #{key}"
    @store.set(key, value)
  end
  # delegate the rest …
end

TypedCache::Decorators.register(:logger, LogDecorator)

Error handling

All operations return one of:

  • Either.right(Snapshot) – success
  • Either.left(CacheMissError) – key not present
  • Either.left(StoreError) – transport / serialization failure

Use the monad directly or pattern-match:

result.fold(
  ->(err)      { warn err.message },
  ->(snapshot) { puts snapshot.value },
)

The CacheRef and Store APIs

While you can call get, set, and fetch directly on the store, the more powerful way to work with TypedCache is via the CacheRef object. It provides a rich, monadic API for a single cache key. The Store also provides fetch_all for batch operations.

You get a CacheRef by calling store.ref(key):

user_ref = store.ref("users:123") # => #<TypedCache::CacheRef ...>

Now you can operate on it:

# Fetch a value, computing it if it's missing
snapshot_either = user_ref.fetch do
  { id: 123, name: "Jane Doe" }
end

# The result is always an Either[Error, Snapshot]
snapshot_either.fold(
  ->(err)      { warn "Something went wrong: #{err.message}" },
  ->(snapshot) { puts "Got value: #{snapshot.value} (from cache: #{snapshot.from_cache?})" }
)

# You can also map over values
name_either = user_ref.map { |user| user[:name] }
puts "User name is: #{name_either.value.value}" # unwrap Either, then Snapshot

Batch Operations with fetch_all

For retrieving multiple keys at once, the Store provides a fetch_all method. This is more efficient than fetching keys one by one, especially with remote back-ends like Redis.

It takes a list of keys and a block to compute the values for any missing keys.

user_refs = store.fetch_all("users:123", "users:456") do |missing_key|
  # This block is called for each cache miss
  user_id = missing_key.split(":").last
  puts "Cache miss for #{missing_key}! Computing..."
  { id: user_id, name: "Fetched User #{user_id}" }
end

user_refs.each do |key, snapshot_either|
  snapshot_either.fold(
    ->(err)      { warn "Error for #{key}: #{err.message}" },
    ->(snapshot) { puts "Got value for #{key}: #{snapshot.value}" }
  )
end

The CacheRef API encourages a functional style and makes composing cache operations safe and predictable.

Instrumentation

TypedCache can publish events about cache operations using different instrumenters. To enable it, use the with_instrumentation method on the builder, specifying an instrumentation backend:

# For ActiveSupport::Notifications (e.g. in Rails)
store = TypedCache.builder
  .with_backend(:memory)
  .with_instrumentation(:rails)
  .build.value

# For Dry::Monitor
store = TypedCache.builder
  .with_backend(:memory)
  .with_instrumentation(:dry)
  .build.value

Events are published to a topic like typed_cache.<operation> (e.g., typed_cache.get). The topic namespace can be configured.

Payload keys include: :namespace, :key, :operation, :duration, and cache_hit.

You can subscribe to these events like so:

# Example for ActiveSupport
ActiveSupport::Notifications.subscribe("typed_cache.get") do |name, start, finish, id, payload|

# Or you can subscribe via the store object itself
instrumenter = store.instrumenter
instrumenter.subscribe("get") do |event|
  payload = event.payload
  puts "Cache GET for key #{payload[:key]} took #{payload[:duration]}ms. Hit? #{payload[:cache_hit]}"
end

If you call with_instrumentation with no arguments, it uses a Null instrumenter, which has no overhead.

Custom Instrumenters

Just like with back-ends and decorators, you can write and register your own instrumenters. An instrumenter must implement an instrument and a subscribe method.

class MyCustomInstrumenter
  include TypedCache::Instrumenter

  def instrument(operation, key, **payload, &block)
    # ... your logic ...
  end

  def subscribe(event_name, **filters, &block)
    # ... your logic ...
  end
end

# Register it
TypedCache::Instrumenters.register(:custom, MyCustomInstrumenter)

# Use it
store = TypedCache.builder
  .with_instrumentation(:custom)
  # ...

Further examples

For more advanced scenarios—including Rails integration, pattern matching, custom back-ends, and testing—see examples.md.

License

This work is licensed under the Apache-2.0 license.

Apache 2.0 License Key Terms

Grants

  • Perpetual, worldwide, non-exclusive, royalty-free license to:
    • Reproduce the work
    • Prepare derivative works
    • Distribute the work
    • Use and sell the work

Requirements

  • Include a copy of the Apache 2.0 License with any distribution
  • Provide attribution
  • Clearly mark any modifications made to the original work
  • Retain all original copyright and license notices

Permissions

  • Commercial use allowed
  • Modification permitted
  • Distribution of original and modified work permitted
  • Patent use granted
  • Private use allowed

Limitations

  • No warranty or liability protection
  • Trademark rights not transferred
  • Contributors not liable for damages

Compatibility

  • Can be used in closed-source and commercial projects
  • Requires preserving original license and attribution