Skip to main content

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 Get or Set style 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

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 FusionCache as HybridCache
  • 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

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 IFusionCache is needed for most cases
  • Consolidated documentation, guidance, and metrics by consuming it from a package
  • Usable outside of the server monorepo
  • 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 IDistributedCache is needed memory cache and backplane can be turned off
    • If no IDistributedCache is needed it can be turned off and only memory and the backplane will be used.
  • Configure and document how to view caches hits and misses for your FusionCache usages

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"
}
}
}
}
}
}