JavaCaching
June 8, 2026

Using Key Value Stores and Cache Patterns in Java Systems

A key-value store is one of the simplest storage models a Java application can use: provide a key, receive the value.

That simplicity is the reason key-value stores are so useful. They are often fast, easy to reason about, and well-suited to temporary or disposable data. They are also a common foundation for caching, session storage, and database offloading.

The danger is treating a cache like a database. A key-value store can improve performance, but it introduces consistency, lifecycle, and failure questions that must be designed explicitly.

The Problem

Many Java services repeatedly read the same data from slower storage. A payment application may read session data, user profile fragments, authorization hints, or frequently used reference data many times during a short period.

Reading everything from a relational database can increase latency and load.

Without cache:
Java service
  |
  v
Database for every read

A key-value store can sit in the middle:

With cache:
Java service
  |
  v
Key-value store
  |
  v
Database only when needed

This improves the average path, but it adds design questions. What happens when the key is missing? How is the cache updated? How long should data stay there? What happens if a node fails?

Core Idea

A key-value store associates a unique key with a value. Most use cases can be modeled with operations as simple as get, put, and delete.

This is similar to a hash table conceptually. The application already knows the key and wants a fast lookup by that key.

Key:
session:8d7a

Value:
serialized session data

Values may be simple strings or serialized objects. The source material mentions that formats such as strings and Protobuf can be used depending on the data.

A key-value store may be purely in memory, which is fast but less durable. Some implementations can persist to disk or to another storage system. That persistence is often asynchronous to reduce the cost of reads and writes.

When Key Value Stores Fit

Key-value stores work best when access is direct and simple.

Good examples include:

  • Session data.
  • Temporary authorization data.
  • Frequently read reference values.
  • Data that can be rebuilt from another store.
  • Cache entries in front of a relational database.
  • Short-lived values where latency matters more than long-term durability.

They are a poor fit when the application needs complex queries on object values. If you need joins, filtering across many fields, or strong structured querying, a relational database may be better.

Good access pattern:
get by known key

Weak access pattern:
find all payments by amount range and recipient country

The more the application needs to inspect the value to find records, the less natural a key-value store becomes.

Replication and Reliability

A key-value store can run as one process, but production systems often use multiple nodes.

Replication means that data changes are propagated from one node to other nodes. This can help the system survive node failure.

Application
  |
  v
Key value node A
  |
  +--> replica node B
  +--> replica node C

Replication can be synchronous or asynchronous.

Synchronous replication reduces the chance of data loss because the write is confirmed only after replicas receive the change. The tradeoff is higher write latency.

Asynchronous replication improves write latency because the primary node can answer earlier. The tradeoff is that a failure may occur before replicas receive the latest data.

Synchronous replication:
safer writes, slower response

Asynchronous replication:
faster response, higher inconsistency risk

This is an architectural decision, not only a configuration detail.

Cache Aside

Cache aside gives the application full control over cache behavior.

The application checks the cache first. If the value is missing, it reads from the persistent store, then places the value in the cache.

Read request
  |
  v
Check the key-value store
  |
  +-- hit --> return cached value
  |
  +-- miss --> read database
              write cache
              return value

A conceptual Java flow can look like this:

public UserProfile findProfile(String userId) {
    UserProfile cached = cache.get("profile:" + userId);

    if (cached != null) {
        return cached;
    }

    UserProfile profile = database.findProfile(userId);
    cache.put("profile:" + userId, profile);
    return profile;
}

Cache aside is flexible. The downside is that every application using the cache must implement the rules correctly.

Read Through and Write Through

In read-through and write-through designs, the application interacts mainly with the key value store. The key value store coordinates with the persistent storage.

Read-through loads missing values from persistent storage. Write-through propagates writes from the key value store to persistent storage.

Application
  |
  v
Key-value store
  |
  v
Persistent storage

This makes the application simpler because it does not need to coordinate both systems directly.

The tradeoff is that the key value store becomes more responsible. The team must understand how it connects to the persistent store and how failures are handled.

Read Behind and Write Behind

Read-behind and write-behind are similar to read-through and write-through, but synchronization is asynchronous.

The application can get faster responses, but the persistent storage may temporarily lag behind the cache.

Application writes cache
  |
  v
Immediate response
  |
  v
Persistent storage updated later

This can be useful when performance matters more than immediate consistency. It can be dangerous if other applications read the persistent storage directly and expect the latest data.

Write Around

In write-around, the application writes directly to persistent storage but reads through the key value store.

This can reduce unnecessary cache writes, but it creates a stale data risk. The cache may still contain old values after the database changes.

Write:
Application -> database

Read:
Application -> cache -> database when missing

To make this safer, the system must notify the cache when persistent data changes. The chapter mentions change data capture as one way to detect changes in the persistent store. After a change, the cache can update the entry or remove it so that the next read reloads fresh data.

Data Lifecycle and Eviction

Memory is limited. A key-value store needs rules for deciding what to remove.

Common lifecycle strategies include:

Strategy Meaning
Least recently used Remove records not accessed recently
Tenure Remove records based on creation time
Least frequently used Remove records with low access counts
Most recently used Remove recently accessed records when repeated access is unlikely

Each strategy matches a different access pattern.

For session data, a tenure-based rule may be useful because old sessions should expire. For frequently reused reference data, least recently used may be more natural. For a one-time processing workflow, the most recently used can make sense if the same key is unlikely to be accessed again soon.

Testing the Cache Design

A cache is not tested only by checking the happy path.

A practical test plan should cover hits, misses, stale data, and failure behavior.

Cache test workflow:
1. Read with empty cache.
2. Confirm database fallback happens.
3. Read again and confirm cache hit.
4. Update persistent data.
5. Confirm cache invalidation or refresh.
6. Stop one cache node.
7. Confirm expected failover behavior.
8. Fill cache past capacity.
9. Confirm eviction behavior.

For Java services, keep cache behavior behind a small interface. That makes it easier to test the service without depending on a real cache server for every unit test.

interface UserSessionStore {
    SessionData find(String sessionId);
    void save(String sessionId, SessionData session);
    void remove(String sessionId);
}

The implementation can use a real key-value store in production and an in-memory fake in tests.

Common Mistakes

The first mistake is assuming cached data is always correct. Cache entries can be missing, stale, or inconsistent with persistent storage.

The second mistake is caching data without an eviction strategy. Memory pressure will eventually force a decision, so make the decision explicit.

The third mistake is choosing asynchronous replication without accepting the risk of data loss or inconsistency.

The fourth mistake is using key-value storage for complex search. If the application requires complex queries, this storage model may be unsuitable.

The fifth mistake is letting cache logic leak everywhere. Keep cache access behind clear service or repository boundaries.

Checklist

  • The use case reads by known key.
  • Cached data can be rebuilt or tolerated as temporary.
  • Cache miss behavior is defined.
  • Cache update behavior is defined.
  • Replication mode is chosen deliberately.
  • Eviction strategy matches access patterns.
  • Stale data rules are known.
  • Write-around usage includes invalidation or change detection.
  • Unit tests can run without a real cache.
  • Production monitoring can show hit rate, misses, and failures.

Conclusion

Key value stores are powerful because they are simple. They give Java systems fast access to temporary, frequently read, or easily rebuilt data.

Use them where key-based lookup is natural. Choose cache patterns deliberately. Define lifecycle rules. Understand replication tradeoffs. A cache should make the system faster without making correctness invisible.

Share:

Comments0

Home Profile Menu Sidebar
Top