Redis vs. MongoDB: When to Use Each, and Why Java Teams Run Both
Redis and MongoDB are not really alternatives to each other. MongoDB is a document database that persists your data; Redis is an in-memory data store that usually sits in front of one. Of the eight articles currently ranking for "Redis vs. MongoDB", seven conclude that you should run both — which makes a different question the useful one: once MongoDB is already in your stack, what does Redis actually buy you?
MongoDB's own comparison page argues the answer is "not much" — a fair argument, and for many applications a correct one, up to a specific and predictable point. Here is where that point is, and how Java teams wire the two together once they pass it.
What Redis and MongoDB Actually Are
MongoDB stores BSON documents in collections, indexes them, and exposes a query language and an aggregation pipeline over them. Its default storage engine, WiredTiger, keeps data on disk and holds recently used pages in an internal cache, backed by whatever the operating system keeps in its filesystem cache. Redis keeps its working set in RAM, persists asynchronously through snapshots or an append-only log, and exposes data structures — strings, hashes, lists, sets, sorted sets, streams — with atomic commands over them rather than a query language. Redis 8 narrowed the gap considerably, adding JSON as a core data structure and a query engine capable of secondary indexing, alongside an AGPLv3 licensing option that Bytebase's widely-cited comparison has yet to catch up with. Valkey, the BSD-licensed fork, behaves identically for everything here; our Valkey vs. Redis comparison covers the split.
The difference that matters is not the data model. It is where the data lives by default, and who owns the memory it lives in.
| MongoDB | Redis / Valkey | |
|---|---|---|
Primary storage |
Disk, with an in-process WiredTiger cache |
RAM, with optional RDB and AOF persistence |
Querying |
Query language, aggregation pipeline, secondary indexes |
Key lookup, plus secondary indexing over defined indexes |
Expiration |
TTL indexes, swept by a background task |
Per-key TTL — invisible on access, reclaimed by an active cycle |
Coordination |
Atomic single-document writes |
Atomic commands, Lua scripts, pub/sub, locks built on both |
Typical role |
System of record |
Cache, session store, queue, lock manager, rate limiter |
Is Redis a NoSQL Database?
Yes. So is MongoDB. They are different kinds of NoSQL database, and conflating the two categories is where most of the confusion here starts. MongoDB is a document store: records are self-describing documents that can vary in shape, and the engine understands their structure well enough to index and query individual fields. Redis is a key-value store whose values happen to be rich data structures rather than opaque blobs — you reach data by key, and what comes back is a hash, a sorted set, a stream, or a JSON document you can read and modify by path.
That distinction has narrowed rather than vanished. Redis can now maintain secondary indexes, run full-text and vector queries over JSON and hash fields, and even aggregate results with FT.AGGREGATE. What it still does not offer is any of that over data you did not plan to index, multi-collection joins, or an ad-hoc query language. Exploratory access patterns belong in MongoDB; known, latency-sensitive ones belong in Redis.
MongoDB's Case Against Redis, and Where It's Right
MongoDB makes the argument on its own comparison page: "If you're already using MongoDB, you can achieve similar results without adding Redis to your system. MongoDB's storage engine, WiredTiger, has an internal cache that may be enough to accommodate your application's working set. In addition, MongoDB Enterprise Advanced offers an in-memory storage engine."
The strongest version of it came in May 2026 from Andrew Morgan, a Senior Staff Developer Advocate at MongoDB, in When Should You Use a Cache With MongoDB? on foojay.io — the Java community's own publication. It works through the usual reasons teams give for adding a cache, rejects each in turn, and concludes: "I've yet to finish a design review without recommending that the cache tier be removed. So to answer the question in the title of this article—when should you use a cache with MongoDB?—the answer is probably never." It reaches that conclusion without a line of code or a single measurement, which is the gap this article is trying to fill.
Much of it is right, and worth conceding before arguing the other side. WiredTiger's internal cache is substantial: on a self-managed deployment it defaults to the larger of 50% of (RAM − 1 GB) or 0.256 GB. Atlas differs — 50% or more of RAM on M40 and larger, but 25% on M30 and smaller. The filesystem cache then holds compressed pages using whatever memory is free. If your working set fits, reads already come from memory, and a cache in front of MongoDB is caching something that never touched disk. TTL indexes really do expire sessions without a scheduled job, and single-document writes really are atomic. For one service, on one replica set, at moderate load, a second datastore is a real cost for little return, and "just use MongoDB" is the correct call.
Four Places the MongoDB-Only Argument Stops Working
Each of the four below is structural rather than a tuning problem. None is a criticism of MongoDB as a database; they are jobs it was not designed to do.
The WiredTiger Cache Belongs to One mongod
MongoDB's documentation is precise about scope: WiredTiger "allocates its cache to the entire mongod instance" and "doesn't allocate cache on a per-database or per-collection level." The default size "assumes that there is a single mongod instance per machine." And a replica set is "a group of mongod instances that maintain the same data set."
There is no shared cache tier anywhere in that picture. Under the default read preference every application instance funnels through the primary's single cache, competing with the write workload on the same box; spread reads across secondaries or shards and you get several caches, each warmed only by the traffic that node happened to serve, and each cold again after a failover. Either way the thing being cached is data pages MongoDB read on the way — nothing your application computed ever enters any of them.
MongoDB Does Not Cache Query Results
This is not an inference. MongoDB's own FAQ states it in one sentence: "MongoDB does not cache the query results in order to return the cached results for identical queries."
It is easy to miss, because MongoDB does maintain a query plan cache and the two get conflated. The plan cache remembers how to execute a shape of query efficiently; it does not remember what that query returned. An aggregation that fans out across collections and takes two hundred milliseconds re-executes in full every time it is asked, and if eight service instances need the same result, MongoDB produces it eight times. MongoDB's guidance on $lookup is that "excessive use of $lookup may slow down query performance," and its recommended fix is to change your data model rather than cache the result.
Caching computed results is the clearest thing an external cache buys you, and the thing MongoDB says outright it does not do. A dashboard aggregate, a personalized feed, a permissions matrix assembled from four collections — a larger WiredTiger cache removes the input I/O for those, but not the work. The aggregation still runs.
MongoDB TTL Indexes Are a Once-a-Minute Sweep, Not an Expiry
TTL indexes are often offered as the reason you do not need Redis for sessions or tokens. They work, with caveats. Per the TTL index documentation, "the background task that removes expired documents runs every 60 seconds," and MongoDB is explicit that "the TTL index does not guarantee that expired data is deleted immediately upon expiration." Deletion is "a single-threaded background task," and "on replica set members, the TTL background thread only deletes documents when a member is in state primary." The indexes are also single-field only.
For log retention, a minute of slack is irrelevant. For a rate-limit window, a one-time token, an idempotency key or a lock lease, it is the whole problem — those need expiry to be a property of the read, not the outcome of a sweep that may not have run yet. Redis checks the TTL on every access, so an expired key is not returned to a read; the background cycle exists to reclaim the memory, not to make the expiry visible. On the Java side, RMapCacheNative gives you per-entry TTL with no eviction task at all, expiring entries server-side — it needs Valkey 9.0+ or Redis 7.4+, and it is in the open-source edition.
MongoDB Has No Lock API — You Build One
MongoDB gives you good raw material. "In MongoDB, write operations are atomic on the single-document level, even if modifying multiple values," which makes findAndModify a correct compare-and-set primitive, and plenty of working locks have been built on exactly that.
But building is what you will be doing. MongoDB's server manual and its Java driver document no application-facing lock API — no lease, no automatic expiry, no fencing token, no renewal while a slow operation is still running. The lockInfo command that surfaces MongoDB's internal locks "is an internal command available on mongod instances only," and reports the server's own concurrency control rather than anything your application can take out. Spring Integration ships lock registries backed by JDBC, Redis and Zookeeper, and Spring Cloud AWS adds one for DynamoDB; there is no MongoDB one. Everything that makes a distributed lock safe — what happens when the holder pauses for a long garbage collection, how the lease is renewed, how a stale holder is detected — is code you write and test yourself.
Using Redis and MongoDB Together in Java
Here is the odd thing about this comparison: of the articles that rank for it, almost none contain any Java. The most-cited Java-framed piece dates from 2020 and has no Java code in it at all, and Redis's own microservices caching walkthrough is written in Node.js.
Stop Hand-Writing Cache-Aside
Nearly every tutorial on caching MongoDB with Redis teaches the same shape: check the cache, and if the value is missing, load it from MongoDB and put it back. Cache-aside is a fine pattern, but hand-writing it means every call site repeats the check, the load, the write-back, and the fallback when the cache is unavailable.
Redisson removes that code. An RMap can be given a MapLoader that populates missing entries from an external source and a MapWriter that propagates changes back to it. With a loader attached, "if requested entry doesn't exist in the Redisson Map object when it will be loaded using provided MapLoader object." A writer in write-through mode blocks until the external store is updated; in write-behind mode, updates "are accumulated in batches and asynchronously written with defined delay to external storage." All of this is in the open-source edition. Redisson ships no MongoDB integration and its own example uses JDBC, so the binding below is ordinary application code — the loader and writer are the only two places that know MongoDB exists.
// Use org.redisson.api.options.MapOptions and org.redisson.api.map.WriteMode
// — a same-named WriteMode exists on the deprecated org.redisson.api.MapOptions.
// Statics: com.mongodb.client.model.Filters.eq / .in, Projections.include
MongoCollection<Document> products = mongo.getCollection("products");
MapLoader<String, Product> loader = new MapLoader<>() {
@Override
public Product load(String id) {
Document doc = products.find(eq("_id", new ObjectId(id))).first();
return doc == null ? null : Product.from(doc);
}
@Override
public Iterable<String> loadAllKeys() {
return products.find()
.projection(include("_id"))
.map(d -> d.getObjectId("_id").toHexString());
}
};
MapWriter<String, Product> writer = new MapWriter<>() {
@Override
public void write(Map<String, Product> entries) {
// Redisson hands over a whole batch — send it as one round trip.
products.bulkWrite(entries.entrySet().stream()
.map(e -> new ReplaceOneModel<>(
eq("_id", new ObjectId(e.getKey())),
e.getValue().toDocument(),
new ReplaceOptions().upsert(true)))
.toList());
}
@Override
public void delete(Collection<String> keys) {
products.deleteMany(in("_id",
keys.stream().map(ObjectId::new).toList()));
}
};
RMap<String, Product> cache = redisson.getMap(
MapOptions.<String, Product>name("products")
.loader(loader)
.writer(writer)
.writeMode(WriteMode.WRITE_BEHIND)
.writeBehindDelay(1000) // set both explicitly:
.writeBehindBatchSize(50)); // they default to 0 here
// A miss is loaded from MongoDB and cached. No cache-aside code.
Product p = cache.get("6512f0c3a1b2c3d4e5f60718");
Set the two write-behind values deliberately rather than inheriting them. The javadoc on the current options interface still states defaults of 1000 milliseconds and 50 entries, but only the deprecated MapOptions.defaults() builder actually applies them — the current builder leaves both at zero, which leaves the size-based flush comparing against nothing. Batching writes is a throughput win and a durability trade, and only you know which side of that your data sits on; our guide to Java caching strategies covers when each fits, and read-through caching summarizes the loader side.
The same loader and writer work on an RLocalCachedMap, which keeps a near cache inside each JVM so repeat reads never leave the process. Instances sharing a name exchange invalidation events over a pub/sub channel, so a write on one node evicts the entry everywhere.
Invalidating the Cache With MongoDB Change Streams
A cache is only as good as its cache invalidation. The MapWriter above covers writes that go through the cache; it cannot see writes that do not — a batch job, an admin tool, another service, a migration script.
MongoDB's answer is change streams. Calling watch() on a MongoCollection from the Java driver returns a stream of change events you can turn into cache evictions, and they are resumable — "change streams are resumable by specifying a resume token to either resumeAfter or startAfter when opening the cursor" — so a consumer that restarts picks up where it left off rather than leaving the cache stale. They require a replica set or sharded cluster, which most production deployments already are. They do not, contrary to a claim that survives in older write-ups, require majority read concern: it "can be either enabled (default) or disabled to use change streams."
One caveat shapes the design. Update events carry deltas, and asking for the full document performs a separate lookup. MongoDB warns that "if there are one or more majority-committed operations that modified the updated document after the update operation but before the lookup, the full document returned may differ significantly from the document at the time of the update operation." The deltas themselves are always sound — they "correctly describe the watched collection changes that applied to that change stream event" — so use the stream to evict rather than to write the looked-up document into the cache. Eviction is idempotent and cannot go stale; writing a possibly-older document can.
Serializing MongoDB Documents Into Redis
Settle serialization early. ObjectId is not a string and does not survive a naive JSON round trip, BSON and Java's Instant disagree on time precision, and serializers that embed Java type information make cached documents markedly larger than the BSON they came from — which matters when the point was to hold more of them in memory. Redisson lets you set the codec per object; our post on serialization codecs covers the options.
In Spring, @Cacheable Has a Redis Backend and No MongoDB One
Spring's caching abstraction is deliberately store-agnostic, but the list of stores it documents is short: a ConcurrentMap-based cache, Ehcache, Caffeine, GemFire and JSR-107. Spring Boot's supported providers add Hazelcast, Infinispan, Couchbase, Cache2k and Redis. MongoDB appears on neither list, and Spring Data MongoDB — currently at 5.1.0 — ships no caching abstraction of its own. So annotate a repository method with @Cacheable in a MongoDB application and you still have to choose a cache store, and MongoDB is not one of the options.
Redisson provides RedissonSpringCacheManager for the role, with ttl and maxIdleTime configurable per cache name in code or YAML, so different caches in one application expire on different schedules; our guide to Spring Boot caching walks through it. There is also a JCache (JSR-107) implementation if you prefer the standard API, and a Live Object mapper that maps annotated Java objects onto Redis structures, much as Spring Data maps them onto MongoDB documents.
Do You Need Both Redis and MongoDB? A Decision Rule
MongoDB alone is enough when all of these hold:
- You run a single service, or a few instances that share no derived state.
- Your working set fits comfortably in WiredTiger's cache.
- Your reads are mostly document lookups rather than expensive aggregations.
- Expiry precision of roughly a minute is acceptable.
- You need neither distributed locking nor a sub-millisecond p99.
There, Redis is a second thing to operate for very little return, and the advice to remove the cache tier is correct.
You want both when any of these is true:
- Horizontally scaled JVMs that would each recompute the same result.
- Aggregations expensive enough to be worth caching, which MongoDB will not do for you.
- Cross-JVM cache invalidation.
- Distributed locks where correctness rather than politeness is at stake.
- Shared sessions, rate limiting, leaderboards or queues.
- A latency target a disk-backed database plus a network hop cannot meet.
Then MongoDB stays the system of record and Redis holds the hot path in front of it. The inverse — Redis as the durable store — is a narrower option, covered in using Valkey or Redis as a primary database in Java.
If your real question is which cache to put in front of MongoDB rather than which database to keep, the other candidate is Memcached — Redis vs. Memcached compares them, and our use cases page goes deeper on the read-through and write-behind patterns.
Redis vs. MongoDB: Frequently Asked Questions
Is Redis a SQL or NoSQL Database?
Redis is NoSQL — specifically a key-value store whose values are data structures such as hashes, sorted sets, streams and JSON documents, rather than a relational database with tables and SQL. MongoDB is also NoSQL, but a document store.
What Are the Disadvantages of Redis?
It holds its working set in RAM, which costs more per gigabyte than disk. It adds a second system to deploy, monitor and fail over. It has no joins and no ad-hoc query language, so access patterns must be planned. And any cache introduces the possibility of serving stale data.
Can Redis Replace MongoDB?
Usually no. Redis can serve as a system of record with the right persistence and capacity planning, but you give up the aggregation pipeline, ad-hoc querying and document-level indexing that made MongoDB attractive.
Is Redis Faster Than MongoDB? Redis vs. MongoDB Performance
For key lookups of data already in memory, yes, by a wide margin. Be skeptical of published performance numbers, though: the most transparent benchmark in circulation, from ScaleGrid, tests Redis 3.2 against Percona Memory Engine rather than stock MongoDB, and much of the newer material offers figures with no disclosed hardware or method. Measure your own workload.
Does MongoDB Cache Query Results?
No. MongoDB's documentation states that it "does not cache the query results in order to return the cached results for identical queries." It caches data pages and query plans, but an identical query re-executes in full every time.
How Do I Cache MongoDB Queries in Spring Boot?
Enable Spring's cache abstraction with @EnableCaching and register a cache manager, since Spring Data MongoDB provides none. With Redisson, RedissonSpringCacheManager sets TTL and max idle time per cache name. For caching whole entities rather than method results, an RMap with a MapLoader pointing at your collection removes the cache-aside code altogether.
Do I Need Redis if I Use MongoDB Atlas?
The same reasoning applies. Atlas offers no managed cache tier, and its semantic cache integration is a client-side library storing vectors in an ordinary collection rather than a cluster feature. Each data-bearing node has its own WiredTiger cache, so the per-node argument above is unchanged — sharper, if anything, on M30 and smaller, where Atlas allocates 25% of RAM to it.