Last modified: April 27, 2026
This article is written in: 🇺🇸
Application-level caching stores computed results or frequently accessed objects directly inside the running process or in a dedicated in-process store. Because data never leaves the application's memory space, reads are limited only by CPU and memory bandwidth — no network hop, no serialisation, and no socket overhead. This makes application-level caches the fastest possible cache tier, complementing distributed caches like Redis or Memcached that serve data across multiple instances.
Application-Level vs Distributed Cache
Single Process Distributed Setup
+--------------------------+ +----------------------------------+
| Application Instance | | App1 App2 App3 App4 |
| | | +---+ +---+ +---+ +---+ |
| +--------------------+ | | |L1 | |L1 | |L1 | |L1 | |
| | In-Process Cache | | | +--++ +--++ +-+-+ +-+-+ |
| | (heap / off-heap) | | | | | | | |
| +--------+-----------+ | | +--+---+--+--+ | |
| | | | | | | |
| v | | +----+----+ +--+----+ | |
| +--------------------+ | | | Redis / | | Redis / | | |
| | Database / Source | | | |Memcached | |Memcached| | |
| +--------------------+ | | +---------+ +---------+ | |
+--------------------------+ +----------------------------------+
In-process cache = L1 (ultra-fast, node-local)
Distributed cache = L2 (fast, cluster-wide)
Database = source of truth
Caffeine is a high-performance, near-optimal Java caching library based on the W-TinyLFU eviction algorithm.
LoadingCache<String, Product> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(5))
.refreshAfterWrite(Duration.ofMinutes(1))
.recordStats()
.build(key -> productRepository.findById(key));
Product p = cache.get("product:42"); // auto-loads on miss
expireAfterWrite resets the TTL after each write, ensuring stale entries are eventually removed even if they remain popular.refreshAfterWrite triggers a background reload after the specified interval while still serving the old value until the refresh completes.maximumSize bounds heap usage, and the eviction algorithm automatically selects the lowest-value entries when the limit is reached.recordStats() method exposes metrics including hit rate, load time, and eviction count via a CacheStats object.Guava Cache is the predecessor to Caffeine and is still widely used in the Java ecosystem.
LoadingCache<String, User> userCache = CacheBuilder.newBuilder()
.maximumSize(5000)
.expireAfterAccess(10, TimeUnit.MINUTES)
.weakValues()
.build(key -> userDao.load(key));
expireAfterAccess removes entries that have not been read within the specified duration, which is useful for session-like data.weakValues() tells Guava to hold values via weak references, allowing the garbage collector to reclaim memory under pressure.Ehcache is a full-featured cache framework widely used in enterprise Java and Spring applications.
<!-- ehcache.xml -->
<cache name="products"
maxEntriesLocalHeap="10000"
timeToLiveSeconds="300"
timeToIdleSeconds="120"
overflowToDisk="true">
</cache>
from functools import lru_cache
from cachetools import TTLCache, cached
@lru_cache(maxsize=512)
def get_config(key: str) -> str:
return db.fetch(key)
_ttl_cache: TTLCache = TTLCache(maxsize=1000, ttl=300)
@cached(cache=_ttl_cache)
def get_user(user_id: int) -> dict:
return user_service.fetch(user_id)
lru_cache uses Python's built-in LRU eviction and is suitable for functions whose inputs have a small, bounded cardinality.cachetools.TTLCache adds time-based expiry on top of LRU eviction, removing entries that are older than the configured TTL.import LRU from 'lru-cache';
const cache = new LRU({
max: 500,
ttl: 1000 * 60 * 5, // 5 minutes
});
function getProduct(id) {
const hit = cache.get(id);
if (hit) return hit;
const product = db.query(id);
cache.set(id, product);
return product;
}
lru-cache is a widely-used npm package providing an LRU cache with optional TTL support.cluster or PM2) each have their own isolated cache, making a distributed layer necessary for cross-process consistency.var cache sync.Map
func GetUser(id string) *User {
if val, ok := cache.Load(id); ok {
return val.(*User)
}
user := db.FetchUser(id)
cache.Store(id, user)
return user
}
sync.Map provides concurrent read and write access without explicit locking, optimised for the case where entries are written once and read many times.groupcache (from the Go team) fills the gap between a simple map and Redis by providing consistent hashing, request coalescing, and hot-key replication across a small cluster of app servers.ristretto from Dgraph offer a more sophisticated eviction algorithm (TinyLFU) and metrics collection for production Go services.Application code manages loading:
func Get(key) -> value:
val = cache.get(key)
if val == nil:
val = db.fetch(key)
cache.set(key, val, ttl)
return val
Caffeine.build(loader) and Spring's @Cacheable annotation both implement the read-through pattern transparently.@CachePut annotation implements write-through by updating the cache entry alongside the database call.refreshAfterWrite implements this pattern by triggering a background reload when a read occurs after the refresh threshold has passed.Invalidation Approaches
Time-based (TTL) Event-based Version-based
+---------------+ +---------------+ +--------------+
| Entry expires | | DB update | | Key includes |
| after N secs | | triggers a | | version or |
| automatically | | cache.delete | | hash suffix |
+---------------+ +---------------+ +--------------+
Simple, no coupling Precise, event-driven No invalidation needed;
Risk: stale data Complexity: messaging old keys expire naturally
@CacheEvict annotation removes specific cache entries by key or clears the entire cache when a method modifies underlying data.Many production architectures combine a local in-process cache (L1) with a distributed cache (L2) for maximum efficiency.
Two-Tier Cache Read Flow
App Instance
+--------------------------------------------+
| 1. Check L1 (Caffeine / lru-cache) |
| | |
| HIT + MISS |
| | | |
| v v |
| Return 2. Check L2 (Redis / Memcached) |
| value | |
| HIT + MISS |
| | | |
| v v |
| Populate L1 3. Fetch from DB |
| Return val | |
| Populate L2 + L1 |
| Return value |
+--------------------------------------------+
CacheStats object provides all four standard metrics and integrates with Micrometer for Prometheus or Datadog export.