Series MapLesson 90 / 112
Focus mode active/Press Alt+Shift+R to toggle/Esc to exit
Deepen PracticeOrdered learning track

Redis Fundamentals and Cache

Redis Fundamentals and Cache Design

Practical production model for Redis as a cache in Java/JAX-RS enterprise services, including cache-aside, TTL, invalidation, serialization, consistency, observability, and failure modes

8 min read1548 words
PrevNext
Lesson 90112 lesson track62–92 Deepen Practice
#redis#cache#cache-aside#ttl+6 more

Part 090 — Redis Fundamentals and Cache Design

Fokus part ini: memahami Redis sebagai cache untuk Java/JAX-RS enterprise service: kapan dipakai, kapan tidak, bagaimana lifecycle cache-aside bekerja, bagaimana mendesain key/TTL/invalidation/serialization, bagaimana menjaga tenant isolation, dan bagaimana men-debug stale data, cache stampede, eviction, latency, dan Redis outage di production.

Catatan penting:

This part does not assume CSG uses Redis, Lettuce, Jedis, Redisson, Redis Cluster,
Redis Sentinel, AWS ElastiCache, Azure Cache for Redis, or any specific cache platform.
Treat Redis usage, client library, topology, and platform standard as internal verification items.

Redis should not be treated as “a faster HashMap”.

Redis is a networked, shared, operational dependency with its own memory limit, eviction policy, persistence behavior, cluster topology, security model, and failure modes.

For senior engineers, the question is not:

Can this query be cached?

The better question is:

Can this data safely be stale, duplicated, evicted, recomputed, invalidated, and observed?

1. Redis Mental Model

Redis is commonly used as:

cache
rate limiter backend
idempotency store
distributed lock backend
coordination primitive
notification channel
job queue / stream backend

This part focuses on Redis as a cache. Later parts cover coordination, locking, rate limiting, streams, and queues.

A Redis cache sits between application code and the source of truth:

sequenceDiagram participant Client as API Client participant API as JAX-RS Service participant Cache as Redis Cache participant DB as Source of Truth Client->>API: GET /catalog/items/{id} API->>Cache: GET catalog:item:{id} alt cache hit Cache-->>API: cached representation API-->>Client: response else cache miss Cache-->>API: nil API->>DB: SELECT item DB-->>API: row/domain data API->>Cache: SET key value EX ttl API-->>Client: response end

The cache is not the owner of truth. The database, external service, catalog engine, pricing engine, or configuration system remains authoritative.


2. When Redis Cache Makes Sense

Redis cache is useful when all of these are mostly true:

The data is read frequently.
The data is more expensive to compute/fetch than to cache.
Some bounded staleness is acceptable.
The cache key can be designed safely.
Invalidation or TTL strategy is understood.
The cached value can be serialized and versioned safely.
Operational metrics can prove benefit.

Typical candidates:

product catalog lookup
tenant configuration snapshot
reference data
feature flag snapshot
permission policy snapshot
pricing rule metadata
external service lookup result
expensive read model

Bad candidates:

highly sensitive secrets
rapidly changing transactional state
authorization decisions without short TTL and strict invalidation
large unbounded objects
values that cannot tolerate stale reads
values with ambiguous tenant/resource ownership
writes that require strong consistency

3. Cache Correctness Vocabulary

TermMeaningProduction concern
Cache hitValue found in Redishit rate may hide stale data
Cache missValue absentmiss storm can overload DB
TTLTime until key expiresstale window vs recomputation cost
EvictionRedis removes key under memory pressurecached data can disappear anytime
InvalidationApp intentionally deletes/updates keyrace conditions and partial invalidation
StalenessCache value older than source of truthacceptable only when bounded/understood
StampedeMany callers recompute same missing valueDB/downstream overload
PenetrationRepeated requests for nonexistent datanegative caching needed
Hot keyOne key receives extreme loadRedis shard/node hotspot
Serialization driftCached value no longer matches codedeploy compatibility issue

Senior rule:

Every cached value must have an explicit correctness contract.

Example:

Tenant product catalog snapshot may be stale for up to 5 minutes.
Quote pricing calculation result must not be cached unless key includes all pricing inputs and validity context.
Permission policy may be cached for 30 seconds and must be invalidated on role change if required by policy.

4. Cache-Aside Pattern

Cache-aside is the most common pattern.

Lifecycle:

1. Application receives read request.
2. Application builds cache key.
3. Application reads Redis.
4. If hit, deserialize and return.
5. If miss, read source of truth.
6. Serialize result.
7. Store in Redis with TTL.
8. Return response.

Pseudo-code:

public CatalogItem getCatalogItem(TenantId tenantId, CatalogItemId itemId) {
    String key = cacheKeys.catalogItem(tenantId, itemId);

    Optional<CatalogItem> cached = redis.get(key, CatalogItem.class);
    if (cached.isPresent()) {
        metrics.increment("catalog.cache.hit");
        return cached.get();
    }

    metrics.increment("catalog.cache.miss");

    CatalogItem item = catalogRepository.findByTenantAndId(tenantId, itemId)
            .orElseThrow(() -> new NotFoundException("catalog item not found"));

    redis.set(key, item, Duration.ofMinutes(5));
    return item;
}

This code is incomplete for production. It still needs:

negative caching
stampede protection
serialization versioning
timeout handling
fallback behavior
metrics
tenant-safe key design
bounded object size

5. Key Design

A Redis key is a contract.

Bad key:

item:123

Better key:

qo:v1:tenant:{tenantId}:catalog:{catalogId}:item:{itemId}

Why better:

service namespace prevents collision
version allows serialization/key evolution
tenant is explicit
resource type is explicit
resource identity is explicit

Key design rules:

Include tenant boundary when data is tenant-specific.
Include environment/service namespace if Redis is shared.
Include version if value shape can change.
Avoid raw PII in keys.
Avoid unbounded user input in keys.
Normalize key component casing/format.
Hash very long identifiers if needed.
Document key ownership and TTL.

Example key builder:

public final class CacheKeys {
    public String catalogItem(TenantId tenantId, CatalogId catalogId, CatalogItemId itemId) {
        return "qo:v1:tenant:%s:catalog:%s:item:%s"
                .formatted(tenantId.value(), catalogId.value(), itemId.value());
    }
}

Do not scatter string concatenation across the codebase. Cache key construction should be centralized and testable.


6. Value Design and Serialization

Redis stores bytes. Your application decides what those bytes mean.

Common formats:

JSON
MessagePack
Protocol Buffers
Java native serialization
String values
Hash fields

Avoid Java native serialization for long-lived/shared cache values unless there is a strong internal standard. It couples cache data to class names, serialVersionUID, and Java implementation details.

Safer cache value strategy:

Use explicit DTO for cached representation.
Include schema/version if needed.
Keep cached value small.
Avoid storing domain entities directly.
Avoid storing lazy ORM/proxy objects.
Do not cache secrets unless explicitly approved.

Example cached DTO:

public record CachedCatalogItemV1(
        String tenantId,
        String catalogId,
        String itemId,
        String name,
        String status,
        Instant effectiveFrom,
        Instant effectiveTo
) {}

Why versioned DTO matters:

During rolling deployment, old pods and new pods may read the same Redis values.
If the value shape changes incompatibly, deployment can fail after partial rollout.

7. TTL Strategy

TTL is a business and operational decision, not only a technical setting.

Questions before choosing TTL:

How often does the source data change?
How stale may the API response be?
What is the cost of recomputation?
What happens if many keys expire together?
Can invalidation happen on write/change event?
Is the data tenant-specific?
Is there a legal/compliance freshness requirement?

Typical TTL categories:

Data typeExample TTLReasoning
static reference datahoursrarely changes, low risk
tenant configminuteschanges occasionally, staleness bounded
permissionsseconds/minutessecurity-sensitive, short staleness
catalog/pricing metadataseconds/minuteseffective-date correctness matters
external lookupminutesprotects downstream service
negative cachesecondsavoid repeated DB miss, limit false stale-not-found

Add jitter to avoid synchronized expiry:

Duration ttlWithJitter(Duration base) {
    long baseSeconds = base.toSeconds();
    long jitter = ThreadLocalRandom.current().nextLong(0, Math.max(1, baseSeconds / 10));
    return Duration.ofSeconds(baseSeconds + jitter);
}

Without jitter, many keys populated at the same time may expire at the same time and trigger a stampede.


8. Invalidation Strategy

There are only a few real invalidation choices:

TTL-only
write-through update
delete-on-write
event-driven invalidation
versioned key namespace
manual/admin invalidation
StrategyStrengthWeakness
TTL-onlysimplestale until TTL expires
delete-on-writeeasy to reason aboutrace with concurrent readers
update-on-writefresh cachewrite path slower and more complex
event-driven invalidationdecoupledevent delay/loss/duplicate risk
versioned namespacesafe for bulk changescan leave old keys until eviction
manual invalidationemergency controlhuman error risk

Example delete-on-write:

public void updateCatalogItem(TenantId tenantId, CatalogItemId itemId, UpdateCommand cmd) {
    catalogRepository.update(tenantId, itemId, cmd);
    redis.delete(cacheKeys.catalogItem(tenantId, cmd.catalogId(), itemId));
}

Race warning:

Reader A misses cache.
Writer updates DB and deletes cache.
Reader A writes old value into cache.

Prevention options:

short TTL
versioned values
double delete with delay
write-through with version check
event timestamp validation
source-of-truth version in cached value

9. Negative Caching

Repeated requests for missing data can overload the database.

Example:

GET /catalog/items/does-not-exist
-> Redis miss
-> DB miss
-> repeat thousands of times

Negative cache stores “not found” for a short TTL.

sealed interface CatalogCacheValue permits CatalogCacheHit, CatalogCacheNotFound {}

record CatalogCacheHit(CachedCatalogItemV1 item) implements CatalogCacheValue {}
record CatalogCacheNotFound(Instant checkedAt) implements CatalogCacheValue {}

Rules:

Use short TTL for negative cache.
Do not negative-cache authorization failures as not-found unless intentionally designed.
Invalidate negative cache when creating the resource.
Track negative-cache hit metrics separately.

10. Cache Stampede Protection

Cache stampede happens when many requests observe the same miss and recompute together.

Common controls:

TTL jitter
single-flight per key inside process
distributed lock with short lease
stale-while-revalidate
request coalescing
pre-warming for known hot keys
rate limiting around recomputation

Single-flight idea:

Only one request per process recomputes a missing key.
Other concurrent requests wait for the same computation result.

Distributed lock warning:

Do not casually add Redis locks to every cache miss.
Locks introduce lease expiry, stuck lock, split-brain, latency, and failure semantics.
Use locks only when recomputation cost/risk justifies it.

Stale-while-revalidate idea:

Return slightly stale cached value quickly.
Refresh asynchronously in background.
Use only when stale value is acceptable.

11. Redis Client Lifecycle in Java

Common Java Redis clients include Lettuce, Jedis, and Redisson. The actual client must be verified internally.

Generic production rules:

Create Redis client once per application lifecycle.
Reuse connections/pools according to client library guidance.
Set connect timeout, command timeout, and shutdown timeout.
Do not block request threads indefinitely on Redis.
Classify Redis failures as dependency failures.
Instrument command latency, timeout, connection count, and error rate.

Example wrapper shape:

public interface CacheClient {
    <T> Optional<T> get(String key, Class<T> type);
    void set(String key, Object value, Duration ttl);
    void delete(String key);
}

Why wrap the client:

centralizes serialization
centralizes timeout/error mapping
centralizes metrics
centralizes key/value policy
makes tests simpler
prevents Redis-specific API leaking everywhere

Avoid:

// Anti-pattern: Redis command scattered inside business code.
redisTemplate.opsForValue().set("item:" + id, entity);

Better:

catalogCache.put(tenantId, catalogId, itemId, cachedItem, ttl);

12. Redis Topology and Operational Reality

Redis may run as:

single instance
primary/replica
Sentinel-managed failover
Redis Cluster
managed cloud cache service

Each topology changes failure behavior.

TopologyBenefitRisk
single instancesimplesingle point of failure
primary/replicaread scaling/failover optionreplication lag
Sentinelautomatic failoverclient compatibility/config complexity
Clusterhorizontal scalekey slot/hash-tag design, multi-key command limits
managed serviceoperational offloadprovider-specific limits and maintenance events

Cluster key note:

If multi-key operations are required in Redis Cluster, keys may need hash tags
so related keys land in the same hash slot. Avoid designing cache correctness
around multi-key operations unless necessary.

13. Memory, Eviction, and Object Size

Redis is memory-bound.

Important questions:

What is maxmemory?
What is the eviction policy?
What is average value size?
What is key cardinality?
What happens when memory is full?
Are large values compressed or rejected?
Can one tenant fill the cache?

Eviction means cache keys can disappear before TTL.

Therefore application code must treat Redis as optional acceleration:

Redis hit -> fast path
Redis miss -> source-of-truth path
Redis unavailable -> degraded source-of-truth path or controlled failure

Never design read correctness such that Redis is the only copy unless you are intentionally using Redis as a primary data store, which is a different architecture decision.


14. Multi-Tenancy and Cache Isolation

Tenant-specific data must be tenant-isolated in cache.

Minimum rule:

Tenant-specific cache key must include tenant boundary.

But key prefix alone may not be enough. Also check:

Is cached value tenant-specific?
Can tenant ID be spoofed from request parameter?
Is tenant resolved from trusted identity/context?
Does invalidation delete only the current tenant's keys?
Can one tenant create unbounded cache cardinality?
Are metrics/logs tenant-safe?

Example failure:

Key: catalog:item:123
Tenant A requests item 123 -> cache populated
Tenant B requests item 123 -> receives Tenant A's value

Corrected key:

qo:v1:tenant:A:catalog:standard:item:123
qo:v1:tenant:B:catalog:standard:item:123

For enterprise CPQ/order systems, tenant-specific catalog, pricing rules, product eligibility, and permission policies require very careful key design.


15. Redis Security Baseline

Redis cache can contain business-sensitive data even if it is “only a cache”.

Security checks:

TLS enabled where required.
Authentication enabled.
Network access restricted.
No public exposure.
Secrets not logged.
PII not stored unless approved.
Tenant-specific values isolated.
Admin commands restricted.
Backups/persistence policy understood.
Data retention expectations documented.

Avoid caching:

raw access tokens
refresh tokens
private keys
unredacted customer documents
sensitive PII without policy approval
full quote/order object if not necessary

Cache only what you need, for as long as you need it.


16. Observability

Minimum Redis/cache metrics:

cache hit count
cache miss count
hit ratio by cache name
get latency
set latency
delete latency
command timeout count
connection error count
serialization error count
value size distribution
key cardinality estimate
eviction count
expired key count
Redis memory usage
Redis CPU usage

Application-level metrics should use stable labels:

cache_name="catalog_item"
operation="get|set|delete"
result="hit|miss|error|timeout|serialization_error"

Avoid high-cardinality labels:

key
tenant_id unless explicitly approved
item_id
quote_id
full exception message

Good structured log example:

{
  "event": "cache.get.completed",
  "cacheName": "catalog_item",
  "result": "miss",
  "durationMs": 3,
  "correlationId": "...",
  "traceId": "..."
}

Do not log full keys when keys contain tenant/customer/resource identifiers unless the logging policy permits it.


17. Redis Failure Handling

Decide per cache whether Redis failure is:

transparent degradation
partial degradation
hard failure

Example policy:

CacheRedis down behaviorReason
catalog reference cachequery DB/source directlycache is acceleration
permission policy cachequery authoritative policy service, maybe fail closedsecurity-sensitive
external lookup cachemaybe return controlled 503downstream may be protected by cache
pricing rule cachedepends on correctness and source availabilitybusiness-critical

Common error mapping:

Redis GET timeout -> log/metric -> fallback to source if allowed
Redis SET timeout -> return source result, record cache write failure if non-critical
Redis serialization error -> treat as cache miss and delete bad key if safe
Redis connection refused -> degrade/fail based on cache criticality
Redis cluster moved/ask errors -> client topology/config issue

Rule:

A cache outage should not automatically become a full API outage unless the cache is explicitly part of the correctness path.

18. Local Development and Testing

Local workflow options:

Docker Compose Redis
Testcontainers Redis-compatible service
fake in-memory cache for unit tests
real Redis for integration tests
cache disabled profile

Test cases to include:

cache hit
cache miss
not found negative cache
serialization incompatibility
Redis timeout
Redis unavailable
TTL expiry
invalidation after write
tenant isolation
stampede control if implemented

Example Docker Compose snippet:

services:
  redis:
    image: redis:7
    ports:
      - "6379:6379"
    command: ["redis-server", "--appendonly", "no"]

Do not assume this matches production. Production may use TLS, auth, cluster, managed service, or different eviction policy.


19. Debugging Playbook

Step 1 — Identify the cache

Which logical cache is involved?
What is the key pattern?
What is the TTL?
What source of truth backs it?
Is stale data acceptable?

Step 2 — Check application metrics

hit ratio
miss rate
latency
timeouts
serialization errors
fallback count
stampede/recompute count

Step 3 — Check Redis health

redis-cli INFO memory
redis-cli INFO stats
redis-cli INFO clients
redis-cli INFO commandstats
redis-cli DBSIZE

Production managed Redis may require provider dashboards instead of direct CLI access.

Step 4 — Check key/value behavior safely

redis-cli TTL <key>
redis-cli TYPE <key>
redis-cli MEMORY USAGE <key>

Avoid dumping sensitive values in shared channels.

Step 5 — Check source of truth

Is the DB/source value already changed?
Was invalidation triggered?
Did event-driven invalidation arrive?
Did a stale writer repopulate the key?

Step 6 — Check deployment compatibility

Are old and new pods reading the same cached value shape?
Was key version bumped for breaking change?
Are mixed versions during rollout safe?

20. Failure Mode Catalog

Failure modeRoot causeDetectionPrevention
stale catalog dataTTL too long or missed invalidationcustomer mismatch, audit, cache inspectbounded TTL, event invalidation, versioned values
cross-tenant leakmissing tenant in keysecurity incidenttenant-aware key builder, tests
stampedesynchronized expiry/hot keyDB spike after cache missjitter, single-flight, lock, stale-while-revalidate
cache penetrationrepeated nonexistent keyshigh DB miss ratenegative cache, input validation, rate limit
huge valuecaching large objectRedis memory spikevalue size limit, compression, avoid cache
serialization driftchanged class/value shapedeserialization errorversioned DTO/key namespace
Redis outage causes API outageno fallback policy5xx spikeclassify cache criticality
eviction breaks assumptionRedis memory pressureevicted keys, miss spikecapacity planning, correct eviction policy
hot key overloadone key too popularhigh Redis CPU/networklocal cache, sharding, request coalescing
insecure cachePII/secret storedaudit/security findingdata classification, redaction, encryption/TLS
runaway cardinalityunbounded key creationmemory growthkey review, TTL, quotas, cardinality metrics

21. PR Review Checklist

For any PR adding/changing Redis cache usage, review:

[ ] What is the source of truth?
[ ] What exact data is cached?
[ ] Is stale data acceptable? For how long?
[ ] Is TTL explicit and justified?
[ ] Is invalidation strategy documented?
[ ] Is tenant boundary included in the key?
[ ] Are raw PII/secrets avoided in key and value?
[ ] Is value serialization explicit and version-compatible?
[ ] Is key construction centralized and tested?
[ ] Is cache miss behavior safe under load?
[ ] Is cache stampede considered?
[ ] Is negative caching needed?
[ ] Are Redis timeouts bounded?
[ ] Does Redis failure degrade or fail? Is that intentional?
[ ] Are metrics/logs/traces added?
[ ] Are tests covering hit, miss, expiry, invalidation, Redis failure, and tenant isolation?

22. Internal Verification Checklist

For CSG/internal codebase verification, check:

Platform and topology
[ ] Is Redis used? For cache only or also locks/rate limit/queue/stream?
[ ] Is it self-managed, AWS ElastiCache, Azure Cache for Redis, or another platform?
[ ] Single node, Sentinel, Cluster, or managed HA?
[ ] Is TLS/auth enabled?
[ ] What is the maxmemory and eviction policy?

Java client
[ ] Which client is used: Lettuce, Jedis, Redisson, Spring Data Redis, custom wrapper?
[ ] Are timeouts standardized?
[ ] Is connection lifecycle managed centrally?
[ ] Are serialization formats standardized?

Cache governance
[ ] Is there a cache naming/key convention?
[ ] Is tenant ID required in tenant-specific keys?
[ ] Is there a value size limit?
[ ] Is TTL policy documented per cache?
[ ] Is invalidation event-driven, write-driven, TTL-only, or manual?

Observability
[ ] Are hit/miss metrics emitted per cache name?
[ ] Are Redis latency and timeout metrics captured?
[ ] Are eviction/memory metrics monitored?
[ ] Are logs redacted?

Operational readiness
[ ] Is there a Redis outage runbook?
[ ] Are cache warmup/preload jobs used?
[ ] Is there an emergency cache flush procedure?
[ ] Who can run it?
[ ] What customer impact occurs during flush/outage?

23. Senior Takeaways

Redis cache is a consistency boundary.

The useful senior questions are:

What correctness guarantee does this cache preserve?
What stale window is acceptable?
What happens when Redis is down?
What happens when keys are evicted early?
What happens when old and new application versions read the same value?
Can one tenant affect another tenant?
Can a cache miss storm overload the source of truth?
Can telemetry prove whether the cache helps or hurts?

A good cache is boring in production. It reduces load, improves latency, degrades safely, and has clear ownership.

A bad cache hides correctness bugs until traffic, deployment, or tenant-specific edge cases expose them.


References to Verify Against Current Platform Standards

Use internal platform documentation as the source of truth. Public references useful for orientation:

Redis cache-aside documentation:
https://redis.io/docs/latest/develop/use-cases/cache-aside/

AWS whitepaper on database caching strategies with Redis:
https://docs.aws.amazon.com/whitepapers/latest/database-caching-strategies-using-redis/caching-patterns.html
Lesson Recap

You just completed lesson 90 in deepen practice. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.