Effect provides a powerful caching system with automatic time-to-live (TTL) management, capacity limits, and built-in memoization for expensive computations.
Overview
The Cache module provides:
Automatic lookup on cache misses
TTL management with configurable expiration
Capacity limits with LRU eviction
Concurrent access with deduplication
Type safety with full TypeScript inference
Basic Usage
Creating a Cache
Create a cache with a lookup function:
import { Cache , Effect } from "effect"
const program = Effect . gen ( function* () {
const cache = yield * Cache . make ({
capacity: 100 ,
lookup : ( key : string ) => Effect . succeed ( key . length )
})
// First access triggers lookup
const result1 = yield * Cache . get ( cache , "hello" )
console . log ( result1 ) // 5
// Second access returns cached value
const result2 = yield * Cache . get ( cache , "hello" )
console . log ( result2 ) // 5 (from cache)
})
Cache with TTL
Add time-to-live for automatic expiration:
import { Cache , Effect } from "effect"
const program = Effect . gen ( function* () {
const cache = yield * Cache . make ({
capacity: 100 ,
lookup : ( key : string ) => Effect . succeed ( key . length ),
timeToLive: "5 minutes"
})
const result = yield * Cache . get ( cache , "hello" )
// Value expires after 5 minutes
})
TTL can be specified as a Duration string ("5 minutes", "1 hour") or Duration object.
Advanced Caching
Dynamic TTL
Set TTL based on the cached value or key:
import { Cache , Effect , Exit } from "effect"
const program = Effect . gen ( function* () {
const cache = yield * Cache . makeWith ({
capacity: 1000 ,
lookup : ( userId : number ) =>
Effect . gen ( function* () {
const user = yield * fetchUser ( userId )
return user
}),
timeToLive : ( exit , key ) => {
if ( Exit . isFailure ( exit )) {
// Short TTL for errors
return "1 minute"
}
const user = exit . value
// Different TTL based on user type
return user . isPremium ? "1 hour" : "5 minutes"
}
})
const user = yield * Cache . get ( cache , 123 )
})
Error Handling
Cache lookup errors are propagated:
import { Cache , Effect } from "effect"
const program = Effect . gen ( function* () {
const cache = yield * Cache . make < string , number , string >({
capacity: 10 ,
lookup : ( key : string ) =>
key === "error"
? Effect . fail ( "Lookup failed" )
: Effect . succeed ( key . length )
})
// Successful lookup
const success = yield * Cache . get ( cache , "hello" )
// Failed lookup propagates error
const failure = yield * Cache . get ( cache , "error" ). pipe (
Effect . catchAll (( error ) => Effect . succeed ( - 1 ))
)
})
Cache Operations
Get and Set
Manually set cache values:
import { Cache , Effect } from "effect"
const program = Effect . gen ( function* () {
const cache = yield * Cache . make ({
capacity: 100 ,
lookup : ( key : string ) => Effect . succeed ( key . length )
})
// Set a value directly
yield * Cache . set ( cache , "hello" , 42 )
// Get returns the manually set value
const result = yield * Cache . get ( cache , "hello" )
console . log ( result ) // 42 (not 5)
})
Check Existence
Check if a key exists without triggering lookup:
import { Cache , Effect } from "effect"
const program = Effect . gen ( function* () {
const cache = yield * Cache . make ({
capacity: 100 ,
lookup : ( key : string ) => Effect . succeed ( key . length )
})
// Check without lookup
const exists1 = yield * Cache . has ( cache , "hello" )
console . log ( exists1 ) // false
// Populate cache
yield * Cache . get ( cache , "hello" )
// Now it exists
const exists2 = yield * Cache . has ( cache , "hello" )
console . log ( exists2 ) // true
})
Get Without Lookup
Retrieve only if cached:
import { Cache , Effect , Option } from "effect"
const program = Effect . gen ( function* () {
const cache = yield * Cache . make ({
capacity: 100 ,
lookup : ( key : string ) => Effect . succeed ( key . length )
})
// Returns None without triggering lookup
const empty = yield * Cache . getOption ( cache , "hello" )
console . log ( empty ) // Option.none()
// Populate cache
yield * Cache . get ( cache , "hello" )
// Now returns Some
const cached = yield * Cache . getOption ( cache , "hello" )
console . log ( cached ) // Option.some(5)
})
Invalidation
Remove entries from the cache:
import { Cache , Effect } from "effect"
const program = Effect . gen ( function* () {
const cache = yield * Cache . make ({
capacity: 100 ,
lookup : ( key : string ) => Effect . succeed ( key . length )
})
yield * Cache . set ( cache , "hello" , 5 )
// Invalidate single key
yield * Cache . invalidate ( cache , "hello" )
// Invalidate all keys
yield * Cache . invalidateAll ( cache )
})
Capacity Management
LRU Eviction
Caches use least-recently-used eviction:
import { Cache , Effect } from "effect"
const program = Effect . gen ( function* () {
const cache = yield * Cache . make ({
capacity: 2 ,
lookup : ( key : string ) => Effect . succeed ( key . length )
})
yield * Cache . get ( cache , "a" ) // Cache: [a]
yield * Cache . get ( cache , "b" ) // Cache: [a, b]
yield * Cache . get ( cache , "c" ) // Cache: [b, c] (a evicted)
const hasA = yield * Cache . has ( cache , "a" )
console . log ( hasA ) // false (evicted)
})
Cache Size
Check current cache size:
import { Cache , Effect } from "effect"
const program = Effect . gen ( function* () {
const cache = yield * Cache . make ({
capacity: 100 ,
lookup : ( key : string ) => Effect . succeed ( key . length )
})
yield * Cache . set ( cache , "a" , 1 )
yield * Cache . set ( cache , "b" , 2 )
const size = yield * Cache . size ( cache )
console . log ( size ) // 2
})
Concurrent Access
Request Deduplication
Concurrent requests for the same key are deduplicated:
import { Cache , Effect } from "effect"
const program = Effect . gen ( function* () {
let lookupCount = 0
const cache = yield * Cache . make ({
capacity: 10 ,
lookup : ( key : string ) =>
Effect . sync (() => {
lookupCount ++
return key . length
})
})
// Multiple concurrent requests
const results = yield * Effect . all ([
Cache . get ( cache , "hello" ),
Cache . get ( cache , "hello" ),
Cache . get ( cache , "hello" )
], { concurrency: "unbounded" })
console . log ( results ) // [5, 5, 5]
console . log ( lookupCount ) // 1 (lookup called only once)
})
Complex Keys
Data Class Keys
Use Data classes for complex keys with structural equality:
import { Cache , Data , Effect } from "effect"
class UserId extends Data . Class <{ id : number }> {}
const program = Effect . gen ( function* () {
const cache = yield * Cache . make ({
capacity: 1000 ,
lookup : ( userId : UserId ) =>
Effect . gen ( function* () {
return yield * fetchUser ( userId . id )
}),
timeToLive: "5 minutes"
})
const userId = new UserId ({ id: 123 })
const user = yield * Cache . get ( cache , userId )
})
Real-World Examples
Database Query Cache
import { Cache , Effect , Schema } from "effect"
import * as Sql from "effect/unstable/sql"
const User = Schema . Struct ({
id: Schema . Number ,
name: Schema . String ,
email: Schema . String
})
const makeUserCache = Effect . gen ( function* () {
const sql = yield * Sql . SqlClient . SqlClient
return yield * Cache . make ({
capacity: 1000 ,
lookup : ( userId : number ) =>
Effect . gen ( function* () {
const rows = yield * sql <{
id : number
name : string
email : string
}> `SELECT * FROM users WHERE id = ${ userId } `
if ( rows . length === 0 ) {
return yield * Effect . fail ( "User not found" )
}
return yield * Schema . decode ( User )( rows [ 0 ])
}),
timeToLive: "10 minutes"
})
})
const getUser = ( userId : number ) =>
Effect . gen ( function* () {
const cache = yield * makeUserCache
return yield * Cache . get ( cache , userId )
})
API Response Cache
import { Cache , Effect } from "effect"
interface ApiResponse {
data : unknown
etag : string
}
const makeApiCache = Effect . gen ( function* () {
return yield * Cache . makeWith ({
capacity: 500 ,
lookup : ( url : string ) =>
Effect . tryPromise ({
try : async () => {
const response = await fetch ( url )
const data = await response . json ()
return {
data ,
etag: response . headers . get ( "etag" ) || ""
}
},
catch : ( error ) => new Error ( `Failed to fetch: ${ error } ` )
}),
timeToLive : ( exit , url ) => {
if ( Exit . isFailure ( exit )) return "30 seconds"
// Cache longer for cacheable endpoints
if ( url . includes ( "/static/" )) return "1 hour"
return "5 minutes"
}
})
})
Computation Memoization
import { Cache , Effect } from "effect"
interface FibInput {
readonly n : number
}
const makeFibCache = Effect . gen ( function* () {
return yield * Cache . make ({
capacity: 1000 ,
lookup : ( input : FibInput ) =>
Effect . gen ( function* () {
if ( input . n <= 1 ) return input . n
const cache = yield * makeFibCache
const a = yield * Cache . get ( cache , { n: input . n - 1 })
const b = yield * Cache . get ( cache , { n: input . n - 2 })
return a + b
})
})
})
const fibonacci = ( n : number ) =>
Effect . gen ( function* () {
const cache = yield * makeFibCache
return yield * Cache . get ( cache , { n })
})
Best Practices
Choose capacity based on expected key count and memory constraints: const cache = yield * Cache . make ({
capacity: 1000 , // Balance memory vs hit rate
lookup: expensiveOperation
})
Set TTL based on how fresh data needs to be: const cache = yield * Cache . make ({
capacity: 100 ,
lookup: fetchData ,
timeToLive: "5 minutes" // Balance freshness vs load
})
Consider different TTL for errors: const cache = yield * Cache . makeWith ({
capacity: 100 ,
lookup: riskyOperation ,
timeToLive : ( exit ) =>
Exit . isFailure ( exit ) ? "30 seconds" : "5 minutes"
})
Clear cache when underlying data changes: const updateUser = ( id : number , data : UserData ) =>
Effect . gen ( function* () {
yield * saveToDatabase ( id , data )
yield * Cache . invalidate ( userCache , id )
})
SQL Cache database query results
Schema Validate cached data