AWS Builder Center

NoSQL on AWS: four stores, three yeses and one no

NoSQL on AWS: four stores, three yeses and one no

The failure mode is always the same shape. A team adopts a NoSQL store for one good reason, then keeps using it for everything, and discovers at 02:00 that the thing it was never designed to do is the thing production now depends on. For search engines the symptom is ranking that is subtly wrong and a dual write that lost a row last Tuesday. For key-value stores it is a query pattern nobody modelled. For caches it is a key that turns out to be shared.
The useful question is not "SQL or NoSQL". It is which single question does this store answer, and what happens the first time I ask it a different one.

The question each service answers

servicethe one question it answerswhat it is bad atshape of the bill
DynamoDB"give me the item at this key, at any scale"anything you did not model up front; ad-hoc queries; joinsper request and per GB; nothing to size
ElastiCache (Redis / Valkey)"have I computed this recently?"being the truth — replication is asynchronousper node-hour, ~$12/month at the small end
MemoryDBthe same, when losing a write is unacceptablecosts multiples of ElastiCacheper node-hour, durable log included
OpenSearch"which documents best match this text?"being written to directly by two writersper node-hour plus storage
DocumentDB"I have a MongoDB application and want it managed"being chosen before the application existsper instance-hour
Keyspaces"wide rows, huge write volume, known partition key"low-volume workloads that would fit in RDSper request, serverless
Neptune / Timestream"how are these connected?" / "what did this metric do?"general-purpose storageper instance-hour / per ingest
A useful discipline from the literature: in an aggregate-oriented store you are choosing an aggregate boundary, not a schema. Everything inside the boundary is retrieved and written atomically; everything across boundaries is your problem. Pick the boundary wrong and the fix is a migration, not an index.

The four decisions in this system

The workload is multi-tenant product search: MySQL holds the truth, an index serves the reads. Four NoSQL decisions fall out of that, and one of them is a refusal.
decisionstorewhystate
the search index is a projection, never a second writerOpenSearchone writer, one truth; the index can be rebuilt from MySQL at any timebuilt (OpenSearch locally)
hot search results are cachedElastiCachethe read path is already eventually consistent, so a short TTL changes the staleness window rather than introducing onetarget
writes are made idempotent by a conditional putDynamoDBPutItem with attribute_not_exists is the entire feature, and it does not lock a row in the relational databasetarget
the source of truth stays relational—the product row and its outbox row must commit together; that is one transaction across two rows, plus Flyway migrations and joinsrejected
That last row is the one worth arguing about. DynamoDB does have transactions. The reason it is not the truth here is not atomicity — it is that the rest of the relational toolchain (a migration tool, a schema the ORM validates at boot, joins for the admin views nobody has written yet) is load-bearing, and giving it up buys nothing this workload needs.
Polyglot persistence is a conclusion, not a starting position. Each store above earned its place by answering a question the others answered badly. A fifth store would need to do the same.

The consistency you actually get

This is the table people skip, and it is the one that produces incidents.
storedefault readwhat "read your write" costswhat a failover does to writes
DynamoDBeventually consistentask for a strongly consistent read — not available on a global secondary indexnothing; replication is synchronous within a Region
ElastiCachewhatever the replica hasread from the primary, or accept stalenessasynchronous replication: a promoted replica can lose recent writes
MemoryDBconsistent on the primaryincludeda durable transaction log is the reason it costs more
OpenSearchnear real time, not real timewait for the refresh (about a second by default) or force one, which is expensivezone-aware replicas keep serving
RDS MySQL (for contrast)consistentfreeMulti-AZ keeps the endpoint and drops every open connection
Two practical consequences in this design. A user who creates a product and immediately searches for it may not find it — the outbox ships within about a second, so the contract is "visible shortly", and the UI has to say so rather than pretend otherwise. And an ElastiCache failover that loses the last few seconds of cache writes is harmless because it is a cache; the same event in a store holding the truth is a data-loss incident. The difference is entirely in what you put there.

The cache key is the whole problem

One concrete bug, because it generalises further than it looks.
The search cache key must contain every input that changes the answer: tenant, query text, brand, category, price bounds, stock filter, sort, page. Spring's default key generator builds a key from all method parameters, which is correct. The bug arrives when somebody shortens it for readability:
1
2
tenant=acme q=laptop → cached under "laptop"
tenant=globex q=laptop → HIT → acme's products returned to globex
That is not a staleness bug. It is a cross-tenant data leak, in a system whose one hard rule is that the tenant comes from the credential and is never defaulted. Key design in a shared cache is an authorisation surface, and a high hit rate is not evidence that it is correct — it is exactly what this bug looks like on a dashboard.
The same rule holds one level down: in a key-value store, the partition key is the access pattern. Choose it for the query you have, and you have chosen against every query you have not thought of yet.

What would change the decision

Being explicit about the triggers is more honest than defending the choice forever:
  • Write volume that a single relational primary cannot take. Wide rows, known partition key, no joins — that is the Cassandra-shaped workload, and Keyspaces is the managed way to have it.
  • A document model the application genuinely has. If the aggregate really is a nested document and every read fetches the whole thing, DocumentDB stops being an odd choice.
  • Sessions or rate limits that must survive a failover. That is MemoryDB rather than ElastiCache, and it is worth the money only when losing a write actually matters.
  • A graph question. "Which products are connected to which" asked recursively is where relational joins stop being reasonable.
None of these is true here yet. When one becomes true, the argument is a paragraph, not a rewrite.

Verified, and not

The projection pattern is real: the outbox drains, the index is rebuilt into a versioned index and promoted by an atomic alias swap, and an end-to-end suite asserts it against real OpenSearch 2.19.6 and MySQL 8.4. ElastiCache and DynamoDB are design, not deployment — the Terraform in this repository has never been applied, because applying it creates billable managed data services. Prices above are order-of-magnitude shapes, not quotes.
The reasoning draws on a NoSQL library of 36 books — the aggregate-oriented modelling and polyglot-persistence argument from NoSQL Distilled, document modelling from Data Modeling for MongoDB and MongoDB Applied Design Patterns, partition-key design from Cassandra: The Definitive Guide, and key and expiration design from Redis in Action. Their common thread is the thing this article is about: in every one of these stores, the data model is the access pattern, decided before the first write and expensive to revisit afterwards.

Questions this page should answer

If the page did its work, each of these is answerable from it alone. The section named after each question is where the argument lives, not a hint.
  1. What is the useful question instead of "SQL or NoSQL", and what are you really choosing when you pick an aggregate-oriented store? — The question each service answers
  2. **Three of the four decisions are yeses and one is a refusal. Why does the truth stay relational — and why is atomicity not the reason?** — The four decisions in this system
  3. Which strongly consistent read is not available in DynamoDB, and what does an RDS Multi-AZ failover do that keeping the endpoint hides? — The consistency you actually get
  4. A user creates a product and immediately searches for it. What do they see, and what does the UI owe them? — The consistency you actually get
  5. A shortened cache key is not a staleness bug. What is it, and why is a high hit rate the symptom rather than the reassurance? — The cache key is the whole problem
  6. Name the four triggers that would make a fifth store the right answer, and say which of them is true today. — What would change the decision
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article