Backend Development 11 min read

Key Considerations and Implementation Strategies for a Local Cache in Java

This article outlines the essential design considerations for building a local cache—such as data structures, size limits, eviction policies, expiration, thread safety, simple APIs, persistence, and blocking mechanisms—and demonstrates concrete Java implementations with code examples.

Architecture Digest
Architecture Digest
Architecture Digest
Key Considerations and Implementation Strategies for a Local Cache in Java

When examining MyBatis source code, the cache subsystem reveals both first‑level and second‑level caches, with the latter offering richer functionality; this article uses that insight to discuss what should be considered when implementing a local cache.

Considerations

The main points include how data is stored, the maximum amount of data, handling of excess entries, expiration, thread safety, a simple interface, and optional persistence.

1. Data Structure

The simplest approach is to use a Map (e.g., ConcurrentHashMap ), while more complex systems like Redis employ various structures such as hashes, lists, sets, and sorted sets backed by linked lists, compressed lists, and skip lists.

2. Object Limit

Local caches run in memory, so a configurable upper bound (e.g., 1024 entries) is typical; once the limit is reached, a removal strategy must be applied.

3. Eviction Strategies

Common policies include LRU (Least Recently Used), FIFO (First‑In‑First‑Out), LFU (Least Frequently Used), SOFT (soft references), and WEAK (weak references), each implemented with appropriate Java collections or reference types.

4. Expiration Time

Caches often support per‑key TTL, which can be enforced via passive checks during get/put operations or active background jobs that periodically purge expired entries.

5. Thread Safety

Because multiple threads may access the cache concurrently, thread‑safe containers like ConcurrentHashMap or explicit synchronization (e.g., MyBatis’s SynchronizedCache ) are required.

6. Simple Interface

A user‑friendly API typically provides get , put , remove , clear , and getSize methods, as illustrated by MyBatis’s Cache interface.

7. Persistence

Persisting cache entries to disk enables recovery after a restart; frameworks like Ehcache support disk persistence, while Redis offers AOF and RDB mechanisms.

8. Blocking Mechanism

To avoid duplicate expensive computations, a blocking cache can lock a key while a value is being loaded, allowing other threads to wait rather than repeat the work.

Implementation

Below are concrete Java snippets that demonstrate each consideration.

1. Data Structure

Map
cache = new ConcurrentHashMap<>();

2. Object Limit

Define a maximum size (e.g., 1024) and enforce it when inserting new entries.

3. Eviction Strategies

LRU can be realized with LinkedHashMap , FIFO with a Queue , LFU by tracking access counts in a HashMap , SOFT with SoftReference , and WEAK with WeakReference .

4. Expiration (Passive)

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

5. Thread Safety

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

6. Simple Interface

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();
}

7. Persistence Example

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

8. Blocking Mechanism Example

public class Memoizer
implements Computable
{
    private final Map
> cache = new ConcurrentHashMap<>();
    private final Computable
c;
    public Memoizer(Computable
c) { this.c = c; }
    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); }
        }
    }
}

Conclusion

The design of a local cache should address data structures, size limits, eviction policies, expiration, thread safety, blocking, a clean API, and optional persistence; additional considerations may arise depending on specific use cases.

JavaCacheMyBatisThread Safetylocal cacheLRUEviction Policy
Architecture Digest
Written by

Architecture Digest

Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.

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.