Backend Development 11 min read

Key Considerations and Implementation Strategies for a Local Cache

This article outlines the essential considerations for designing a local cache—including data structures, size limits, eviction policies, expiration, thread safety, blocking mechanisms, simple APIs, and persistence—and demonstrates implementation approaches with Java code examples such as Map usage, synchronized caches, and memoization techniques.

Top Architect
Top Architect
Top Architect
Key Considerations and Implementation Strategies for a Local Cache

Preface

When reading MyBatis source code, the author noticed the caching module and decided to summarize the key points that should be considered when implementing a local cache. MyBatis provides first‑level and second‑level caches; the latter is more feature‑rich and comparable to dedicated cache frameworks.

Considerations

1. Data Structure

The simplest approach is to store cached data in a Map . More complex systems like Redis use various structures such as hash, list, set, and sorted set, backed by linked lists, zip lists, integer sets, and skip lists.

2. Object Limit

Because a local cache resides in memory, a maximum number of cached objects (e.g., 1024) is usually defined, and a strategy is needed to evict excess entries once the limit is reached.

3. Eviction Policy

Common policies include LRU (Least Recently Used), FIFO (First In First Out), LFU (Least Frequently Used), SOFT (soft references), and WEAK (weak references). LRU is often implemented with LinkedHashMap , FIFO with a queue, LFU by tracking access counts, and SOFT/WEAK with SoftReference or WeakReference .

4. Expiration Time

Cache entries can be given a TTL. Two deletion strategies exist: passive deletion (checked during get/put ) and active deletion (a background job periodically removes expired entries).

if (System.currentTimeMillis() - lastClear > clearInterval) {
    clear();
}

5. Thread Safety

Use thread‑safe containers such as ConcurrentHashMap or wrap non‑thread‑safe structures with synchronization. MyBatis, for example, uses a SynchronizedCache wrapper.

public synchronized void putObject(Object key, Object object) {
    // ...
}

@Override
public synchronized Object getObject(Object key) {
    // ...
}

6. Simple API

A user‑friendly cache should expose basic methods like get , put , remove , clear , and getSize . MyBatis defines a Cache interface, and Guava provides a concise Cache API.

public interface Cache {
    String getId();
    void putObject(Object key, Object value);
    Object getObject(Object key);
    Object removeObject(Object key);
    void clear();
    int getSize();
    ReadWriteLock getReadWriteLock();
}
public interface Cache<K, V> {
    V getIfPresent(Object key);
    V get(K key, Callable<? extends V> loader) throws ExecutionException;
    ImmutableMap<K, V> getAllPresent(Iterable<?> keys);
    void put(K key, V value);
    void putAll(Map<? extends K, ? extends V> m);
    void invalidate(Object key);
    void invalidateAll(Iterable<?> keys);
    void invalidateAll();
    long size();
    CacheStats stats();
    ConcurrentMap<K, V> asMap();
    void cleanUp();
}

7. Persistence

Persisting cache data to disk allows recovery after a restart. Ehcache supports disk persistence, while Guava does not. Redis offers AOF and RDB persistence mechanisms.

diskPersistent="false" // whether to persist cache to disk

8. Blocking Mechanism

To avoid duplicate expensive computations, a blocking cache can lock a key while it is being populated. An example from "Java Concurrency in Practice" uses ConcurrentHashMap with FutureTask to ensure only one thread computes the value.

public class Memoizer<A, V> implements Computable<A, V> {
    private final Map
> cache = new ConcurrentHashMap<>();
    private final Computable
c;
    public Memoizer(Computable
c) { this.c = c; }
    @Override
    public V compute(A arg) throws InterruptedException, ExecutionException {
        while (true) {
            Future
f = cache.get(arg);
            if (f == null) {
                Callable
eval = () -> c.compute(arg);
                FutureTask
ft = new FutureTask<>(eval);
                f = cache.putIfAbsent(arg, ft);
                if (f == null) { f = ft; ft.run(); }
                try { return f.get(); }
                catch (CancellationException e) { cache.remove(arg, f); }
            }
        }
    }
}

Summary

The article reviews the essential aspects of designing a local cache—data structure, size limit, eviction policy, expiration, thread safety, blocking, simple API, and persistence—while providing concrete Java code snippets to illustrate each point.

JavaCacheConcurrencyPersistencelocal cacheEviction
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.