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.

1. Lesson 1: Why NoSQL Exists Demo 2. Lesson 2: Relational vs. NoSQL: The Fundamental Differences Demo 3. Lesson 3: The Distributed Systems Foundations of NoSQL Demo 4. Lesson 4: Document Databases Demo 5. Lesson 5: Key-Value Databases Demo 6. Lesson 6: Wide-Column Databases Locked 7. Lesson 7: Graph Databases Locked 8. Lesson 8: Data Modeling in NoSQL Locked 9. Lesson 9: Partitioning and Sharding Locked 10. Lesson 10: Replication Locked 11. Lesson 11: Consistency Models Locked 12. Lesson 12: CAP Theorem in Practice Locked 13. Lesson 13: Transactions in NoSQL Locked 14. Lesson 14: ACID, BASE, and Eventual Consistency Locked 15. Lesson 15: Handling Relationships Without Relational Joins Locked 16. Lesson 16: MongoDB Architecture and Design Locked 17. Lesson 17: Redis Architecture and Use Cases Locked 18. Lesson 18: Cassandra Architecture and Data Modeling Locked 19. Lesson 19: DynamoDB Architecture and Design Locked 20. Lesson 20: Graph Databases with Neo4j Locked 21. Lesson 21: NoSQL in Microservices Locked 22. Lesson 22: NoSQL, Caching, CQRS, and Event-Driven Architecture Locked 23. Lesson 23: Choosing the Right Database: Architecture Decision Framework Locked 24. Lesson 24: Designing a Polyglot Persistence System Locked 25. Lesson 25: Assigning Data Ownership to Services Locked 26. Lesson 26: Designing the Database Architecture Locked 27. Lesson 27: Designing the Data Flow Locked 28. Lesson 28: Consistency, Replication, and Failure Scenarios Locked 29. Lesson 29: Scaling and Performance Architecture Locked 30. Lesson 30: Defending the Architecture: ADR and Architectural Trade-offs Locked

Lesson 5: Key-Value Databases

Demo

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.