Key-Value Store Design — DynamoDB, Redis & Cassandra Architecture Guide
In this tutorial, you'll learn about Key. We cover key concepts, practical examples, and best practices.
A key-value store is a distributed hash map that maps unique keys to values, optimized for high-throughput point lookups and writes, using techniques like consistent hashing, replication, and LSM-tree storage engines.
Why Key-Value Store Design Matters
Web-scale applications need sub-millisecond access to billions of records. Traditional relational databases with full SQL overhead can't match the raw throughput of a key-value store. Amazon DynamoDB handles 10+ trillion requests per day. Redis serves millions of operations per second in-memory at Twitter. Each key-value store makes different tradeoffs — Redis prioritizes speed, DynamoDB prioritizes managed scalability, Cassandra prioritizes availability. Doda Browser's session store uses Redis for sub-millisecond user session lookups across millions of concurrent users.
Plain-Language Explanation
Imagine a giant warehouse where every item has a unique barcode. You give the barcode to a robot, and it retrieves the item. You can't search by color, size, or price — only by barcode. But the robot is incredibly fast because it goes directly to the shelf. That's a key-value store. The key is the barcode, the value is the item. You get blazing-fast lookups and writes, but you sacrifice the ability to query by anything other than the key.
graph TD
Client --> Router[Request Router]
Router --> Ring[Consistent Hash Ring]
Ring --> N1[Node 1
Primary: A-C]
Ring --> N2[Node 2
Primary: D-F]
Ring --> N3[Node 3
Primary: G-I]
Ring --> N4[Node N
Primary: ...]
N1 --> R1[Replica: Node 2]
N2 --> R2[Replica: Node 3]
N3 --> R3[Replica: Node 4]
N4 --> R4[Replica: Node 1]
Router --> Coord[Coordination
Gossip / SWIM]
style Router fill:#3498db,color:#fff
style Ring fill:#e67e22,color:#fff
style Coord fill:#9b59b6,color:#fff
Redis Data Structures
import redis
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
r.set("user:1001", '{"name": "Alice", "email": "alice@example.com"}')
user_data = r.get("user:1001")
print(f"User: {user_data}")
r.rpush("recent_searches:1001", "python", "redis", "system design")
searches = r.lrange("recent_searches:1001", 0, -1)
print(f"Recent searches: {searches}")
r.zadd("leaderboard", {"player1": 1500, "player2": 1200, "player3": 1800})
top = r.zrevrange("leaderboard", 0, 2, withscores=True)
print(f"Top players: {top}")
Expected output:
User: {"name": "Alice", "email": "alice@example.com"}
Recent searches: ['python', 'redis', 'system design']
Top players: [('player3', 1800.0), ('player1', 1500.0), ('player2', 1200.0)]
Cassandra Data Model
Cassandra uses a wide-column model with partition keys:
CREATE KEYSPACE myapp WITH replication = {
'class': 'SimpleStrategy',
'replication_factor': 3
};
CREATE TABLE myapp.user_sessions (
user_id TEXT,
session_id TEXT,
created_at TIMESTAMP,
expires_at TIMESTAMP,
metadata MAP<TEXT, TEXT>,
PRIMARY KEY (user_id, session_id)
) WITH CLUSTERING ORDER BY (session_id DESC);
CREATE TABLE myapp.user_activity (
user_id TEXT,
activity_time TIMESTAMP,
activity_type TEXT,
details TEXT,
PRIMARY KEY (user_id, activity_time)
) WITH CLUSTERING ORDER BY (activity_time DESC)
AND default_time_to_live = 604800;
from cassandra.cluster import Cluster
cluster = Cluster(['127.0.0.1'])
session = cluster.connect('myapp')
session.execute(
"INSERT INTO user_sessions (user_id, session_id, created_at, expires_at) "
"VALUES (%s, %s, %s, %s)",
('user1001', 'sess_abc123', datetime.now(), datetime.now() + timedelta(hours=1)),
)
rows = session.execute(
"SELECT * FROM user_sessions WHERE user_id = %s",
('user1001',)
)
for row in rows:
print(f"Session: {row.session_id}, expires: {row.expires_at}")
LSM-Tree Storage Engine
Cassandra and LevelDB use Log-Structured Merge Trees:
import json, os
class LSMTree:
def __init__(self, path: str):
self.path = path
self.memtable = {}
self.memtable_size = 0
self.flush_threshold = 1024 * 1024
os.makedirs(f"{path}/sstables", exist_ok=True)
self.sstable_count = 0
def put(self, key: str, value: str):
self.memtable[key] = value
self.memtable_size += len(key) + len(value)
if self.memtable_size >= self.flush_threshold:
self.flush()
def get(self, key: str) -> str:
if key in self.memtable:
return self.memtable[key]
return self.search_sstables(key)
def flush(self):
sstable_path = f"{self.path}/sstables/{self.sstable_count}.json"
with open(sstable_path, "w") as f:
for k, v in sorted(self.memtable.items()):
f.write(json.dumps({"key": k, "value": v}) + "\n")
self.memtable = {}
self.memtable_size = 0
self.sstable_count += 1
self.compact()
def compact(self):
pass
def search_sstables(self, key: str) -> str:
for i in range(self.sstable_count - 1, -1, -1):
with open(f"{self.path}/sstables/{i}.json") as f:
for line in f:
record = json.loads(line)
if record["key"] == key:
return record["value"]
return None
Common Mistakes
Hot partitions: A single popular key (like a celebrity's profile) gets hammered. DynamoDB can throttle at the partition level. Use cache or spread the load.
Large values: Values over 1MB degrade performance. In Redis, large values block the event loop. Store metadata in KV store, blobs in object storage.
No TTL for ephemeral data: Session data accumulates forever. Set TTL on all expirable data. Redis and DynamoDB support native TTL.
Scanning instead of querying: Key-value stores aren't built for scans. If you need range queries, choose DynamoDB with sort keys or Cassandra with clustering columns.
Ignoring network latency: Every GET/PUT is a network round-trip. Use client-side caching (Redis cache aside) and pipeline operations.
Practice Questions
What is the difference between Redis and Cassandra? Redis is an in-memory data structure server with persistence options. Cassandra is a disk-based distributed wide-column store. Redis is faster; Cassandra stores more data and tolerates partitions better.
How does DynamoDB handle scaling? DynamoDB automatically partitions data by hash of the partition key. Each partition handles up to 3000 RCU or 1000 WCU. When a partition fills, DynamoDB splits it.
What is an LSM-tree? A write-optimized data structure that buffers writes in a memory table, flushes to immutable SSTables on disk, and periodically compacts them. Used by Cassandra, LevelDB, RocksDB.
Why is consistent hashing used in key-value stores? It minimizes key redistribution when nodes are added or removed. Only K/N keys need to move (where K is total keys and N is node count), instead of nearly all keys.
When should you use a key-value store over a relational database? Use a key-value store for high-throughput point lookups (session stores, caches, user profiles). Use a relational database for complex queries, joins, and transactions.
Mini Project
Build a key-value store server:
from flask import Flask, request, jsonify
import json
app = Flask(__name__)
store = {}
@app.route("/<key>", methods=["PUT"])
def put(key):
store[key] = request.data.decode()
return {"ok": True}, 201
@app.route("/<key>", methods=["GET"])
def get(key):
val = store.get(key)
if val is None:
return {"error": "not found"}, 404
return jsonify({"key": key, "value": val})
@app.route("/<key>", methods=["DELETE"])
def delete(key):
store.pop(key, None)
return {"ok": True}
@app.route("/_scan", methods=["GET"])
def scan():
prefix = request.args.get("prefix", "")
return {"keys": [k for k in store if k.startswith(prefix)]}
if __name__ == "__main__":
app.run(port=8000)
Test it:
curl -X PUT -d "hello world" http://localhost:8000/greeting
curl http://localhost:8000/greeting
curl -X DELETE http://localhost:8000/greeting
Cross-References
- Distributed Caching
- Consistent Hashing
- Database Sharding
- Blob Store Design
- Caching
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro