NoSQL Databases: Architecture, Data Models, and Practical Use
Understand why NoSQL databases emerged and how their architectures differ from relational systems. This tutorial explores document, key-value, wide-column, and graph databases; data modeling; partitioning; sharding; replication; consistency; CAP; transactions; caching; CQRS; microservices; and polyglot persistence. It concludes with practical architecture decisions for ownership, data flow, scaling, performance, and failure scenarios.
Lesson 5: Key-Value Databases
The key-value model takes a much simpler approach to data access by treating information primarily as values associated with unique keys. This lesson explores why that simplicity can produce extremely fast access and makes the model particularly useful for caching, sessions, temporary state, and other workloads where predictable key-based access is more important than complex querying. Redis and DynamoDB will provide concrete examples of this approach.
What Is a Key-Value Database?
A key-value database stores information as a collection of keys and their corresponding values. The key uniquely identifies an item, while the value contains the data associated with that key. The model is conceptually similar to a dictionary or Dictionary<TKey, TValue> in C#.
For example, an application could store a user's shopping cart like this:
Key: cart:user:12345
Value: { "items": [ ... ] }
The application does not need to understand relationships between tables or perform joins to retrieve the cart. It simply provides the key and asks the database for its value.
var cart = await cache.GetStringAsync("cart:user:12345");
This simplicity is the main idea behind the key-value model.
Why Key-Based Access Can Be Extremely Fast
A key-value database can be very fast because the primary operation is simple: find the value associated with a known key. The system does not normally need to perform complex joins or search through unrelated records.
A simplified conceptual operation looks like this:
Application
|
| GET "user:12345"
v
Key-Value Database
|
| lookup key
v
Value
Many key-value systems use in-memory data structures, hash-based lookups, efficient indexing, and distributed partitioning. Redis, for example, is primarily an in-memory data store, which allows very low-latency operations.
However, speed is not automatic. Network latency, serialization, large values, poor key design, and database configuration can still become bottlenecks. The architectural advantage comes from the simplicity of the access pattern.
Redis as a Key-Value Database
Redis is one of the best-known key-value technologies. Although it is commonly described as a key-value store, Redis provides several data structures, including strings, hashes, lists, sets, sorted sets, and streams.
A simple operation can look like this:
SET user:12345:name "John"
GET user:12345:name
In a .NET application, the commonly used StackExchange.Redis library can be used to communicate with Redis:
var db = connection.GetDatabase();
await db.StringSetAsync(
"user:12345:name",
"John"
);
var name = await db.StringGetAsync(
"user:12345:name"
);
Redis is therefore useful when an application needs extremely fast access to relatively simple pieces of state.
TTL and Expiration
One particularly useful feature of key-value databases is TTL, or Time To Live. TTL allows an item to automatically expire after a specified period.
For example, an application might store a password-reset token for only 15 minutes:
await db.StringSetAsync(
"reset-token:abc123",
"user:12345",
TimeSpan.FromMinutes(15)
);
After the TTL expires, Redis automatically removes the item. This is useful for temporary data such as verification codes, authentication state, rate-limit counters, temporary locks, and cached data.
TTL is important architecturally because it allows the database itself to manage the lifecycle of temporary information rather than requiring a background process to periodically clean it up.
Caching
One of the most common uses of Redis is caching. Suppose an application frequently retrieves product information from PostgreSQL. If the same product is requested thousands of times, repeatedly querying the relational database can create unnecessary load.
A cache can sit in front of the database:
Client
|
Application
|
+----> Redis Cache
| |
| Cache Hit
|
+----> SQL Database
|
Cache Miss
The application first checks Redis. If the value exists, it returns it immediately. If it does not exist, the application retrieves the data from the primary database and stores it in Redis for future requests.
This is commonly called the cache-aside pattern.
var cached = await db.StringGetAsync($"product:{id}");
if (!cached.IsNullOrEmpty)
return Deserialize<Product>(cached);
var product = await repository.GetAsync(id);
await db.StringSetAsync(
$"product:{id}",
Serialize(product),
TimeSpan.FromMinutes(10)
);
return product;
Caching improves performance, but it introduces another architectural concern: stale data. The architect must decide how long cached data can remain outdated and how the application should invalidate or refresh it.
Sessions and Distributed State
Key-value stores are also useful for storing user sessions. Consider an application running on several web servers:
Load Balancer
/ | \
/ | \
Server A Server B Server C
\ | /
Redis
If session information is stored only in Server A's memory, a subsequent request routed to Server B may not know anything about the user's session.
By storing the session in Redis, all application instances can access the same state:
session:user:12345
-> {
"userId": 12345,
"role": "Customer"
}
This makes the application servers more stateless and allows them to scale horizontally more easily.
Distributed State
The same principle applies to other types of shared application state. A distributed application may need shared counters, rate-limit information, temporary locks, feature flags, or coordination data.
For example, a rate limiter could maintain a counter such as:
rate:user:12345
-> 47
The application can increment this value and expire it after a specific period. Because multiple application instances access the same Redis cluster, the rate limit can apply consistently across the entire application rather than separately on each server.
This is one reason key-value stores are particularly valuable in microservice and cloud-native architectures.
DynamoDB and the Key-Value Model
Amazon DynamoDB is a managed NoSQL database that supports both key-value and document-oriented data models. Its basic access pattern is based on a partition key, with an optional sort key.
For example, an application's users could be represented using:
Partition Key: UserId
Sort Key: ItemType
This allows the application to efficiently retrieve items using their keys rather than scanning the entire dataset.
A conceptual example might be:
UserId ItemType Data
12345 PROFILE John Smith
12345 ORDER#1001 Order details
12345 ORDER#1002 Order details
The partition key determines where the data is distributed, while the sort key can organize related items within that partition. This design makes DynamoDB particularly useful for applications that know their access patterns in advance.
Key-Value vs. Relational Databases
A relational database might answer:
"Find all customers who placed an order over $1,000 last month and join them with their payment information."
A key-value database is better suited to a question such as:
"Give me the session for user 12345."
The difference is not simply that one database is faster. The databases are optimized for different access patterns.
Relational databases provide flexible queries, relationships, joins, constraints, and transactions. Key-value databases provide extremely efficient access when the application already knows the key and does not require complex relational queries.
When Key-Value Databases Are a Good Choice
Key-value databases are strong candidates for caching, sessions, temporary data, rate limiting, counters, distributed locks, shopping carts, feature flags, and other high-speed state management scenarios.
They are less appropriate when the application frequently needs complex filtering, arbitrary queries, joins, referential integrity, or analytical operations across many records. In those situations, a relational or another specialized database may be a better fit.
Architectural Perspective
The key-value model teaches an important architectural principle: simple access patterns can enable extremely efficient distributed systems. Instead of asking the database to discover relationships and perform complex operations, the application provides a key that directly identifies the required data.
Redis is particularly useful for high-speed temporary or cached state, while DynamoDB can provide highly scalable persistent storage based on well-defined access patterns. Both demonstrate that database selection should begin with the application's workload rather than the database's popularity.
The key idea is that a key-value database is optimized around direct access by key. Its simplicity enables high performance and horizontal scalability, making it especially useful for caching, sessions, temporary state, and workloads with predictable access patterns.