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]