Skip to main content

Entity Caching

Cache entities to reduce database load and improve authorization latency.

Setup

Add the Caffeine module:

build.sbt
libraryDependencies += "io.github.devnico" %% "cedar4s-caffeine" % "0.0.0-SNAPSHOT"

Configure caching:

import cedar4s.caffeine.{CaffeineEntityCache, CaffeineCacheConfig}

val cache = CaffeineEntityCache[Future](CaffeineCacheConfig.default)

val store = EntityStore.builder[Future]()
.register[Entities.User, String](new UserFetcher(db))
.register[Entities.Document, String](new DocumentFetcher(db))
.withCache(cache)
.build()

Cache Flow

Configuration Presets

PresetMax SizeTTLUse Case
default10,0005 minGeneral purpose
highThroughput100,0001 min write, 30s accessHigh request volume
shortLived50,00030 secFrequently changing data
large500,00030 minStable data, large entity sets
testing1001 secUnit tests
// Use a preset
val cache = CaffeineEntityCache[Future](CaffeineCacheConfig.highThroughput)

Custom Configuration

import scala.concurrent.duration.*

val config = CaffeineCacheConfig(
maximumSize = 50_000,
expireAfterWrite = Some(2.minutes),
expireAfterAccess = Some(30.seconds),
recordStats = true
)

val cache = CaffeineEntityCache[Future](config)

Configuration Options

OptionDescriptionDefault
maximumSizeMaximum number of cached entities10,000
expireAfterWriteTTL from when entity was cached5 minutes
expireAfterAccessTTL from last accessNone
recordStatsEnable hit/miss statisticsfalse

TTL Guidelines by Entity Type

Entity TypeSuggested TTLReasoning
Users5+ minChanges infrequently
Roles/Permissions10+ minVery stable
Documents1-5 minBalance freshness vs performance
Sessions30s-1 minSecurity consideration
Audit logsNo cacheWrite-heavy, rarely read

Cache Invalidation

Use .buildCaching() to access invalidation methods:

val store = EntityStore.builder[Future]()
.register[Entities.User, String](new UserFetcher(db))
.withCache(cache)
.buildCaching()

// Invalidate specific entity
store.invalidate(CedarEntityUid("MyApp::User", "user-123"))

// Invalidate all entities of a type
store.invalidateType("MyApp::User")

// Clear entire cache
store.invalidateAll()

You can also invalidate with typed IDs:

// Typed invalidation (uses CedarEntityType + Bijection)
store.invalidateEntity[Entities.User, UserId](userId)
store.invalidateTypeOf[Entities.User]

When to Invalidate

Invalidate when entity data changes:

class UserService(store: CachingEntityStore[Future], db: Database) {

def updateUser(userId: String, updates: UserUpdates): Future[User] = {
for {
user <- db.updateUser(userId, updates)
_ <- store.invalidate(CedarEntityUid("MyApp::User", userId))
} yield user
}

def deleteUser(userId: String): Future[Unit] = {
for {
_ <- db.deleteUser(userId)
_ <- store.invalidate(CedarEntityUid("MyApp::User", userId))
} yield ()
}
}

Cascade Invalidation

When entity relationships change, invalidate related entities. You must provide a childrenOf function that returns the set of child entity type names for a given parent entity type:

// Define the childrenOf function based on your schema
def childrenOf(entityType: String): Set[String] = entityType match {
case "MyApp::Folder" => Set("MyApp::Document") // Documents are children of Folders
case "MyApp::Organization" => Set("MyApp::Workspace", "MyApp::User") // Workspaces and Users are children of Organizations
case _ => Set.empty
}

// Invalidate folder and all cached documents in it
store.getCache.invalidateWithCascade(
CedarEntityUid("MyApp::Folder", folderId),
childrenOf
)

Or with typed IDs:

store.getCache.invalidateEntityWithCascade[Entities.Folder, FolderId](
folderId,
childrenOf
)

The childrenOf function determines which entity types should be invalidated when a parent changes. For example, if a folder is deleted, you may want to invalidate all documents in that folder.

Cache Statistics

Monitor cache performance:

store.cacheStats.map { stats =>
stats.foreach { s =>
println(s"Hit rate: ${s.hitRate}")
println(s"Size: ${s.size}")
println(s"Evictions: ${s.evictionCount}")
}
}

Key Metrics

MetricTargetAction if Low
Hit Rateabove 80%Increase TTL or cache size
Eviction Rateunder 10%Increase maximum size

Custom Cache Backends

Implement EntityCache[F] for custom backends (Redis, Memcached, etc.):

trait EntityCache[F[_]] {
def get(uid: CedarEntityUid): F[Option[CedarEntity]]
def getMany(uids: Set[CedarEntityUid]): F[Map[CedarEntityUid, CedarEntity]]
def put(entity: CedarEntity): F[Unit]
def putMany(entities: Iterable[CedarEntity]): F[Unit]
def invalidate(uid: CedarEntityUid): F[Unit]
def invalidateEntity[A, Id](id: Id)(implicit ev: CedarEntityType.Aux[A, Id], bij: Bijection[String, Id]): F[Unit]
def invalidateType(entityType: String): F[Unit]
def invalidateTypeOf[A](implicit ev: CedarEntityType[A]): F[Unit]
def invalidateWithCascade(uid: CedarEntityUid, childrenOf: String => Set[String]): F[Unit]
def invalidateEntityWithCascade[A, Id](id: Id, childrenOf: String => Set[String])(
implicit ev: CedarEntityType.Aux[A, Id],
bij: Bijection[String, Id]
): F[Unit]
def invalidateAll(): F[Unit]
def stats: F[Option[CacheStats]]
}

No-Op Cache

For testing or when caching should be disabled:

val noCache = EntityCache.none[Future]