0028 - Adopt FusionCache
| ID: | ADR-0028 |
|---|---|
| Status: | ACCEPTED |
| Published: | 2025-12-01 |
Context and problem statement
Numerous caching approaches currently exist in the server codebase, generally entailing the setup
of an instance of IDistributedCache through keyed services. That cache is then injected into some
service and some of the following things are done:
- A hard-coded prefix is added to every
GetorSetstyle call - Entry options are hard-coded, or have minimal customization from
GlobalSettings - The value is serialized, often in JSON format
- If a value isn't found in the cache, a hard-coded value is used instead
- If a value isn't found in the cache, a value from the database is retrieved and used instead
- The value from the database is often cached in the distributed cache
- An even faster copy of the data is stored in memory
- Either in a local field or in
IMemoryCache - Messages from other nodes are subscribed to in order to keep the memory copy in lockstep
- Either in a local field or in
One of the key things that is missing from that list is that no metrics are ever recorded on if the cache was useful, the most important metric being "Was the value stored in the cache ever retrieved before it expired?"
Considered options
- Continue implementing custom caching solutions
- Adopt
HybridCache - Adopt
FusionCache - Adopt
FusionCacheasHybridCache - Implement other third-party libraries
Continue implementing custom caching solutions
Pros
- Maximum customizability by building everything yourself
Cons
- A lot of boilerplate that's easy to get wrong
- No automatic metrics
- Have to manually configure connection strings and TTL
- No standard on key prefixes
Adopt HybridCache
Adopt out-of-box library from Microsoft: HybridCache.
Pros
- Great support and docs
- L1 (in-memory, fast) and L2 (distributed, persistent) cache
- Cache stampede protection (multiple requests simultaneously computing the same value)
- Serialization configured through dependency injection (DI)
Cons
- Key prefixing is still manual
- No memory cache synchronization of nodes, often called a backplane
Adopt FusionCache
Adopt third-party package: FusionCache.
Pros
- Automatic key prefixing (setup in DI)
- L1 and L2 cache
- Cache stampede protection
- Memory cache node synchronization mechanism
- Very customizable
Cons
- Third-party package introduces potential support / maintenance risks
Adopt FusionCache as HybridCache
It's possible to use FusionCache under the hood but inject and interact with HybridCache.
Pros
- All pros from Adopt
FusionCache - If we ever switched to
HybridCache, it would be a DI-only change
Cons
- All cons from Adopt
FusionCache - Abstraction overhead
- Hides the true implementation
Implement other third-party libraries
CacheManager is not as popular or active compared to FusionCache. It does have
built in serialization but it relies on Newtonsoft.Json and therefore would not be AOT friendly.
We'd likely want to implement our own using System.Text.Json. It isn't clear if it has cache
stampede protection. It also doesn't have built in metrics.
LazyCache does not fit our needs in a few ways, most importantly it is only a memory
cache. It has also not released a new version since September 2021.
CacheTower is less popular and less active than FusionCache. The only feature it
is missing that we want is built in metrics.
Decision outcome
Chosen option: Adopt FusionCache, because the FusionCache library contains all the features
we need for our most complex scenarios and has enough customizability for our simpler scenarios.
While HybridCache is impressive and would have the support of Microsoft it lacks the backplane
extensibility so that we can synchronize the L1 cache of all our nodes.
Positive consequences
- Able to get started quickly; nothing but injecting
IFusionCacheis needed for most cases - Consolidated documentation, guidance, and metrics by consuming it from a package
- Usable outside of the
servermonorepo - Customizable without any code changes needed
Negative consequences
- Could make caching too easy to use when caching isn't the right solution all the time
Plan
- New features desiring cache should use
IFusionCache - Finish up Caching package in server SDK
- Individual migration plans for existing cache uses
- If only
IDistributedCacheis needed memory cache and backplane can be turned off - If no
IDistributedCacheis needed it can be turned off and only memory and the backplane will be used.
- If only
- Configure and document how to view caches hits and misses for your
FusionCacheusages
Today
We will use the AddExtendedCache available in server. You are able to call
it with your own name and settings and then inject your own IFusionCache from keyed services using
the given name.
In the near future
Caching will be built into our server SDK and supplied through a Bitwarden.Server.Sdk.Caching
library. With no additional DI registration you will be able to inject IFusionCache using keyed
services. You will then be able to configure your specific instance using named options or through
configuration like such:
{
"Caching": {
"Uses": {
"MyFeature": {
"Fusion": {
"DefaultEntryOptions": {
"Duration": "00:01:00"
}
}
}
}
}
}