NoSQL is a toolbox, not a promotion from relational databases.
Avoid Cargo-Cult NoSQL: Recognize Workloads Better Served by Relational Databases
Recognize when relational transactions, joins, constraints, reporting, and operational simplicity are the better fit, and quantify the application complexity created by unnecessary NoSQL decomposition.
Identify workloads where multi-row constraints, transactions, ad hoc joins, mature SQL/reporting, and modest scale make a relational database the simpler correctness boundary.
Quantify the coordination, denormalization, synchronization, and repair work introduced when a relational workload is forced into multiple NoSQL stores.
Distinguish legitimate NoSQL access-pattern benefits from cargo-cult claims about scale, schema flexibility, or latency.
Use relational and NoSQL systems together when their optimization targets differ instead of demanding one universal database.
1. A “modern” architecture can be a regression
AtlasMart’s checkout service has modest data volume, rich support/reporting queries, unique identifiers, and a correctness requirement linking orders, payments and inventory reservations. A team proposes three independently updated NoSQL stores because the company is “moving to NoSQL.” Nothing in that statement explains the query, invariant, latency, scale or failure problem being solved.
A relational database remains an excellent fit when related facts need strong multi-row constraints or transactions, operators need mature SQL and reporting, and the workload fits a topology the team can run. The right comparison is not old versus new. It is where complexity lives.
2. The relational engine already implements expensive coordination logic
Foreign keys, unique constraints, isolation, transactions, indexes, query optimization and crash recovery move large classes of correctness into the database boundary. Replacing that boundary with independent key/document writes may improve a specific access path, but then the application must own partial failure, retries, duplicate side effects, stale projections and reconciliation. That trade can be correct; it should never be invisible.
| Workload signal | Relational fit | Possible NoSQL reason |
|---|---|---|
| Many ad hoc joins/reports | Strong | Only if precomputed access patterns are stable and worth write fan-out |
| Cross-row uniqueness/invariants | Strong | If invariant can be redesigned into one atomic key/aggregate |
| Simple key lookup at huge scale | Possible but may be overfeatured | Key-value/document can simplify serving path |
| Append-heavy time buckets | Possible | Wide-column/time-oriented design may map better |
| Relationship traversal | Join-heavy | Graph may make relationships first-class |
| Full-text/vector relevance | Limited/native varies | Dedicated search/vector projection may fit retrieval semantics |
3. Deliberately wrong approach: decompose before proving a bottleneck
The broken design writes an order, decrements inventory and captures payment in separate stores. A failure after the inventory write leaves an order and stock mutation without a payment. Now the team needs compensation, idempotency, reconciliation, durable workflow state and monitoring—work that was not required by the original relational problem.
This is not an argument that distributed workflows are always wrong. They become appropriate when business ownership, scale, latency, availability or autonomy justifies them. The error is paying that complexity tax without evidence.
4. AtlasMart lab: compare the correctness boundary
Python 3.13+ with the built-in sqlite3 module.
SQLite is used as a free local relational demonstration, not
as a recommendation for AtlasMart production scale.
import sqlite3
print("RELATIONAL TRANSACTION BOUNDARY")
conn=sqlite3.connect(":memory:")
conn.execute("PRAGMA foreign_keys=ON")
conn.executescript("""
CREATE TABLE orders(id TEXT PRIMARY KEY, customer_id TEXT NOT NULL, total INTEGER NOT NULL CHECK(total >= 0));
CREATE TABLE payments(order_id TEXT PRIMARY KEY REFERENCES orders(id), status TEXT NOT NULL);
CREATE TABLE inventory(sku TEXT PRIMARY KEY, available INTEGER NOT NULL CHECK(available >= 0));
INSERT INTO inventory VALUES('sku-1', 1);
""")
try:
conn.execute("BEGIN")
conn.execute("INSERT INTO orders VALUES('o-1','c-1',1200)")
conn.execute("UPDATE inventory SET available=available-1 WHERE sku='sku-1'")
conn.execute("INSERT INTO payments VALUES('o-1','captured')")
conn.commit()
except Exception:
conn.rollback()
print("orders=", conn.execute("SELECT count(*) FROM orders").fetchone()[0])
print("inventory=", conn.execute("SELECT available FROM inventory WHERE sku='sku-1'").fetchone()[0])
print("payment=", conn.execute("SELECT status FROM payments WHERE order_id='o-1'").fetchone()[0])
print("\nFORCED MULTI-STORE NOSQL WORKFLOW")
order_store={}
inventory_store={"sku-1":1}
payment_store={}
order_store['o-2']={"customer":"c-2","total":900}
inventory_store['sku-1']-=1
print("inject failure before payment write")
print("order exists=", 'o-2' in order_store, "inventory=", inventory_store['sku-1'], "payment exists=", 'o-2' in payment_store)
print("invariant broken: business now needs compensation/reconciliation")
print("\nAD HOC JOIN-LIKE QUESTION")
rows=conn.execute("""
SELECT o.id,o.customer_id,p.status,i.available
FROM orders o JOIN payments p ON p.order_id=o.id CROSS JOIN inventory i
WHERE p.status='captured'
""").fetchall()
print("relational answer=", rows)
print("forced NoSQL answer requires application fan-out or precomputed projection")
The relational path commits order, payment and inventory together and answers a join-like question directly. The intentionally decomposed dictionary stores fail between writes, leaving a partial business state that requires new repair machinery.
5. Production judgment
Keep relational databases in the candidate set whenever they meet the workload. “NoSQL has no schema” and “SQL cannot scale” are not valid decision rules. Likewise, do not force every workload into a relational engine when another data model materially reduces query or scale complexity.
Polyglot architecture begins with restraint: add a second persistence model only when its benefit exceeds synchronization, recovery, security, observability and skill cost. Once a new system is justified, migration becomes the next risk. Lesson 3 treats migration as a temporary distributed system with explicit source-of-truth and rollback states.
Check your understanding
- What is cargo-cult NoSQL?
- Why can a normalized relational design be operationally simpler for checkout?
- Does relational mean “cannot scale horizontally”?
- What cost appears when normalized relationships are manually denormalized?
- When is NoSQL still appropriate beside a relational core?
Review the answers
1. Choosing a NoSQL architecture because of fashion or generic scale claims rather than a workload requirement that benefits from its access, distribution, or data-model tradeoffs.
2. It can keep related rows, constraints and transactions inside one mature consistency boundary and answer ad hoc questions with joins rather than synchronization code.
3. No. Relational systems can replicate, partition and scale in several ways; the point is to compare concrete system limits and requirements, not slogans.
4. Write fan-out, stale copies, repair/reconciliation, event ordering, duplicate handling, backfill logic, and more application ownership.
5. When another workload has a genuinely different access model or scale profile, such as cache/session keys, time-oriented telemetry, graph traversal, or derived search.
References
Foundational and current implementation references used for this lesson:
- PostgreSQL 18 — Transactions — Current official transaction semantics used as one relational implementation anchor.
- PostgreSQL 18 — Logical Replication — Current official snapshot-plus-change replication model relevant to migration and synchronization.
- Debezium 3.6 release series — Latest stable 3.6 line; 3.6.1.Final was released 2026-08-04. Optional CDC implementation anchor only.
- Apache Cassandra downloads — Current official release page listing Cassandra 5.0.9 as latest GA on 2026-08-07.
- PostgreSQL 18 — Constraints — Current official reference for relational integrity constraints.
- PostgreSQL 18 — Table Expressions — Current official join/query reference used to reconnect prerequisite SQL concepts.