Back to Hub
Databases
DatabasesIntermediate

NoSQL — Types, Trade-offs & Use Cases

#NoSQL#MongoDB#Redis#Cassandra#DynamoDB#document#key-value#wide-column#graph#CAP

NoSQL — Types, Trade-offs & Use Cases

NoSQL ("Not only SQL") databases break away from the relational model. Each NoSQL type is purpose-built for a specific access pattern. Knowing which to reach for — and when relational is actually better — is a crucial engineering skill.


Why NoSQL?

Relational databases are excellent general-purpose tools, but they struggle with:

  • Massive scale — sharding a relational DB is complex; many NoSQL databases were designed for horizontal scale from the start
  • Schema flexibility — evolving schemas require migrations; documents can evolve freely
  • Specific access patterns — time-series, graph traversal, and caching have better-fit models
  • Very high write throughput — relational locking mechanisms become bottlenecks

Not a silver bullet: NoSQL databases often sacrifice ACID guarantees, complex querying, and joins.


The Four NoSQL Categories

1. Key-Value Stores

The simplest model: a giant distributed hash map.

Key            → Value
"user:42"      → { name: "Alice", age: 30 }
"session:abc"  → "user:42"
"rate:ip:1.2.3.4" → "47"

Examples: Redis, DynamoDB (also document), Riak, etcd

Redis — In-Memory Key-Value + Data Structures:

javascript
const redis = createClient({ url: 'redis://localhost:6379' }); // Strings await redis.set('user:42:name', 'Alice', { EX: 3600 }); // TTL 1h await redis.get('user:42:name'); // "Alice" // Counters (atomic!) await redis.incr('page:home:views'); // Hashes (object-like) await redis.hSet('user:42', { name: 'Alice', age: '30' }); await redis.hGet('user:42', 'name'); // "Alice" await redis.hGetAll('user:42'); // { name: 'Alice', age: '30' } // Lists (queue / stack) await redis.lPush('jobs:pending', JSON.stringify(job)); await redis.rPop('jobs:pending'); // dequeue (FIFO) // Sets (unique members) await redis.sAdd('online:users', '42', '99', '123'); await redis.sIsMember('online:users', '42'); // 1 // Sorted Sets (leaderboard!) await redis.zAdd('leaderboard', [ { score: 9500, value: 'Alice' }, { score: 8800, value: 'Bob' }, ]); await redis.zRange('leaderboard', 0, 9, { REV: true }); // top 10 // Pub/Sub await publisher.publish('notifications:42', JSON.stringify(event)); await subscriber.subscribe('notifications:42', handler);

Use cases: Caching, sessions, rate limiting, leaderboards, pub/sub, distributed locks, real-time analytics.

Performance: Sub-millisecond reads/writes. ~100,000+ operations/second single node.


2. Document Stores

Stores semi-structured documents (typically JSON/BSON). Documents within a collection can have different schemas.

json
// User document { "_id": "64a1b2c3d4e5f6789abc0001", "name": "Alice", "email": "alice@example.com", "address": { "city": "Tokyo", "country": "Japan" }, "tags": ["premium", "verified"], "orders": [ { "id": "ord_123", "amount": 99.99, "date": "2024-01-15" } ] }

Examples: MongoDB, Firestore, CouchDB, DynamoDB

MongoDB:

javascript
const db = client.db('myapp'); const users = db.collection('users'); // Insert await users.insertOne({ name: 'Alice', age: 30, tags: ['premium'] }); await users.insertMany([...]); // Query await users.findOne({ email: 'alice@example.com' }); await users.find({ age: { $gte: 18, $lte: 65 }, tags: 'premium' }).toArray(); // Projection (select specific fields) await users.findOne({ _id: id }, { projection: { name: 1, email: 1, _id: 0 } }); // Update await users.updateOne( { _id: id }, { $set: { name: 'Alice Smith' }, // set fields $push: { tags: 'verified' }, // append to array $inc: { loginCount: 1 }, // increment $unset: { temporaryCode: '' }, // remove field } ); // Aggregation Pipeline const result = await users.aggregate([ { $match: { age: { $gte: 18 } } }, // filter { $group: { _id: '$country', count: { $sum: 1 }, avgAge: { $avg: '$age' } } }, { $sort: { count: -1 } }, { $limit: 10 }, ]).toArray(); // Indexes await users.createIndex({ email: 1 }, { unique: true }); await users.createIndex({ tags: 1 }); await users.createIndex({ name: 'text', bio: 'text' }); // full-text search

When MongoDB beats SQL:

  • Rapidly evolving schemas (startup MVP)
  • Documents with nested arrays (orders with line items, articles with comments)
  • Content management — each content type has different fields
  • Read-heavy with complex nested access patterns

MongoDB pitfalls:

  • No multi-document ACID before v4 (now supported)
  • Joins ($lookup) are expensive vs SQL
  • Easy to denormalise too much → data consistency issues

3. Wide-Column Stores

Rows have a fixed primary key, but can have millions of different columns. Optimised for time-series and write-heavy workloads.

Examples: Apache Cassandra, HBase, ScyllaDB, Google Bigtable

sql
-- Cassandra Query Language (CQL) CREATE TABLE sensor_readings ( sensor_id UUID, timestamp TIMESTAMP, value FLOAT, unit TEXT, PRIMARY KEY (sensor_id, timestamp) ) WITH CLUSTERING ORDER BY (timestamp DESC); -- Optimised for: "give me readings for sensor X from last hour" SELECT * FROM sensor_readings WHERE sensor_id = ? AND timestamp > now() - 1h; -- Write throughput: Cassandra handles millions of writes/second -- Data is append-only (like a log) — immutable by design

Cassandra architecture:

  • No single point of failure — peer-to-peer
  • Data partitioned by primary key hash across nodes
  • Eventual consistency by default; tunable per-query
  • ConsistencyLevel.QUORUM for strong consistency

Use cases: IoT time-series, event logs, activity feeds, write-heavy analytics, global replication.


4. Graph Databases

Data modelled as nodes (entities) and edges (relationships). Relationship traversal is first-class.

Examples: Neo4j, Amazon Neptune, ArangoDB

cypher
-- Neo4j Cypher query language -- Create nodes and relationships CREATE (alice:User {name: 'Alice', age: 30}) CREATE (bob:User {name: 'Bob', age: 25}) CREATE (alice)-[:FOLLOWS {since: date('2024-01-01')}]->(bob) -- Find friends of friends MATCH (user:User {name: 'Alice'})-[:FOLLOWS*2]->(fof:User) WHERE NOT (user)-[:FOLLOWS]->(fof) RETURN fof.name -- Shortest path between two nodes MATCH path = shortestPath( (alice:User {name: 'Alice'})-[:FOLLOWS*]-(bob:User {name: 'Bob'}) ) RETURN path -- Recommendation: "who do people I follow also follow?" MATCH (me:User {name: 'Alice'})-[:FOLLOWS]->(friend)-[:FOLLOWS]->(rec) WHERE NOT (me)-[:FOLLOWS]->(rec) AND me <> rec RETURN rec.name, COUNT(*) AS commonFriends ORDER BY commonFriends DESC LIMIT 10

Use cases: Social graphs, fraud detection, recommendation engines, knowledge graphs, network topology.

Why not SQL for graphs? In SQL, traversing 5 hops in a social graph requires 5 expensive JOINs. In Neo4j, pointer chasing through adjacency lists is O(log n) regardless of depth.


CAP Theorem in Practice

         Consistency
              │
              │
Availability──┼──Partition Tolerance

Under a network partition, you must choose between Consistency (all nodes see the same data) and Availability (every request gets a response):

SystemChoiceExample
PostgreSQL, MySQLCP (Consistency)Refuses writes during partition
Cassandra, DynamoDBAP (Availability)Serves potentially stale reads
HBaseCPMaster-based, may be unavailable during partition
MongoDBConfigurableTunable via write concern
Redis ClusterAPMay serve stale data from replicas

Choosing the Right NoSQL Type

NeedUse
Caching, sessions, rate limitingRedis (key-value)
Flexible schema, nested documentsMongoDB (document)
High-write time-series or event logsCassandra (wide-column)
Social graphs, recommendations, fraudNeo4j (graph)
Full-text searchElasticsearch
Global multi-region replicationDynamoDB, Cosmos DB, CockroachDB

When to Stick with SQL

Don't reach for NoSQL by default. Relational databases handle 95% of use cases well:

  • ✅ Complex queries with JOINs across multiple entities
  • ✅ Strong ACID requirements (financial transactions)
  • ✅ Schema stability (enterprise, regulated industries)
  • ✅ Reporting and analytics (window functions, aggregations)
  • ✅ Team knows SQL; don't add operational complexity without clear benefit

The best database is usually PostgreSQL until you have a specific problem it can't solve.