top of page

Designing a Cache System: A Comprehensive Approach to Building a Redis-like Solution

Introduction

In today's digital landscape, speed and responsiveness are paramount to delivering exceptional user experiences. When applications need to process millions of requests per second with minimal latency, cache systems become critical components of modern architectures. A cache system like Redis serves as a high-performance data storage layer that sits between applications and their primary databases, significantly reducing access times and server loads.

Cache systems have become increasingly important as businesses scale their digital operations globally. They are essential for handling traffic spikes, reducing database load, and maintaining consistent performance across distributed systems. Companies like Facebook, Twitter, and Amazon rely heavily on sophisticated caching mechanisms to deliver seamless experiences to their massive user bases.

What is a Cache System?

A cache system is an intermediary high-speed data storage layer that stores a subset of data, typically transient, so future requests for that data can be served faster. Unlike primary storage systems designed for persistence and durability, caches prioritize speed and are typically memory-based to achieve microsecond response times.

Modern cache systems like Redis offer rich functionality beyond simple key-value storage, including:

  • Data structures support (lists, sets, sorted sets, hashes)

  • Pub/sub messaging capabilities

  • Transaction support

  • Scripting facilities

  • High availability through replication

  • Automatic partitioning with cluster mode

These systems operate on the principle that frequently accessed data should be kept as close as possible to the processing units, minimizing the time and resources required to retrieve information.

Requirements and Goals of the System

Functional Requirements

  1. Fast Data Access: Provide sub-millisecond data retrieval times

  2. Support for Multiple Data Types: Store strings, lists, sets, sorted sets, hashes

  3. Configurable Eviction Policies: Allow specification of how data is removed when memory limits are reached (LRU, LFU, etc.)

  4. Expiration Mechanism: Support time-to-live (TTL) for cached items

  5. Concurrency Control: Handle multiple concurrent read/write operations

  6. Scalability Support: Allow horizontal scaling through sharding or clustering

  7. Basic Persistence: Provide options to persist data to disk periodically

  8. Connection Management: Handle thousands of client connections simultaneously

Non-Functional Requirements

  1. High Performance: Achieve response times under 1 millisecond

  2. Reliability: Maintain availability during component failures

  3. Scalability: Scale horizontally to handle increasing loads

  4. Consistency Model: Provide configurable consistency levels (eventual vs. strong)

  5. Resource Efficiency: Optimize memory usage and CPU utilization

  6. Monitoring Capabilities: Enable real-time monitoring of cache performance

  7. Security: Provide authentication and authorization mechanisms

Capacity Estimation and Constraints

Traffic Estimates

  • Assume our cache system needs to handle 100,000 requests per second

  • With a peak of 200,000 requests during high traffic periods

  • 80% read and 20% write operations (typical for cache workloads)

Storage Requirements

  • Average size of key: 64 bytes

  • Average size of value: 1 KB

  • Total number of unique items: 10 million

  • Total raw storage needed: 10 million × (64 bytes + 1 KB) ≈ 10.6 TB

Memory Constraints

  • Since caches are primarily memory-based, we need to estimate RAM requirements

  • Not all data needs to be cached (typically 20% of data serves 80% of requests)

  • Required cache storage: 20% of 10.6 TB ≈ 2.12 TB

  • With replication factor of 2 for high availability: 4.24 TB total RAM needed

Bandwidth Requirements

  • For 100,000 requests/second with 1KB average response size

  • Read bandwidth: 80,000 × 1 KB = 80 MB/s

  • Write bandwidth: 20,000 × 1 KB = 20 MB/s

  • Total bandwidth: 100 MB/s (800 Mbps)

System APIs

Our cache system should expose simple, intuitive APIs for basic operations:

// Set a key with a value
SET(key: string, value: string, expiry: int = -1): boolean
// Parameters:
// - key: identifier for the cached item
// - value: data to be stored
// - expiry: optional TTL in seconds, -1 means no expiration
// Returns: true if successful, false otherwise

// Get a value by key
GET(key: string): string
// Parameters:
// - key: identifier for the cached item
// Returns: cached value or null if not found

// Delete a key
DEL(key: string): boolean
// Parameters:
// - key: identifier to be removed
// Returns: true if deleted, false if key didn't exist

// Check if a key exists
EXISTS(key: string): boolean
// Parameters:
// - key: identifier to check
// Returns: true if key exists, false otherwise

// Increment a counter stored at key
INCR(key: string, increment: int = 1): int
// Parameters:
// - key: identifier for counter
// - increment: amount to increase (default 1)
// Returns: new value after increment

For data structures, additional specialized APIs would be provided:

// List operations
LPUSH(key: string, value: string): int
RPOP(key: string): string

// Hash operations
HSET(key: string, field: string, value: string): boolean
HGET(key: string, field: string): string

// Set operations
SADD(key: string, member: string): boolean
SISMEMBER(key: string, member: string): boolean

Database Design

Cache systems typically don't employ traditional database designs but rather specialized in-memory data structures optimized for speed. However, we still need to define our data organization:

Primary Data Structures

  1. Dictionary/Hash Table: The core mapping structure linking keys to values

    • Provides O(1) average lookup time

    • Implemented using optimized hash tables with collision resolution

  2. Expiration Dictionary: Maps keys to their expiration timestamps

    • Enables efficient TTL management

    • Typically implemented as a minimum heap or sorted data structure

  3. LRU/LFU Tracking Structure: Maintains access patterns for eviction policies

    • LRU (Least Recently Used): Doubly-linked list for tracking access order

    • LFU (Least Frequently Used): Frequency counter with aging mechanism

Persistence Structures (Optional)

For systems requiring durability:

  1. Append-Only File (AOF): Records all write operations for replay during restart

  2. Point-in-time Snapshots (RDB): Periodic full-memory dumps to disk

Memory Management

Unlike traditional databases that use disk storage, our cache system will primarily use RAM:

  1. Memory Allocator: Custom allocator to reduce fragmentation

  2. Slab Allocation: Pre-allocated memory chunks of different sizes to reduce waste

  3. Compression: Optional in-memory compression for values to increase effective capacity

High-Level System Design

┌─────────────────────────────────────────────────────────────────┐
│                        Client Applications                       │
└───────────────────────────────┬─────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────┐
│                      Load Balancer/Proxy                         │
└───────────────────────────────┬─────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────┐
│                        Cache Cluster                             │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐          │
│  │ Cache Node 1│    │ Cache Node 2│    │ Cache Node n│          │
│  │ (Primary)   │◄──►│ (Replica)   │◄──►│             │          │
│  └─────────────┘    └─────────────┘    └─────────────┘          │
└───────────────────────────────┬─────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────┐
│                     Configuration & Management                   │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐          │
│  │ Monitoring  │    │ Cluster     │    │ Admin       │          │
│  │ System      │    │ Management  │    │ Interface   │          │
│  └─────────────┘    └─────────────┘    └─────────────┘          │
└─────────────────────────────────────────────────────────────────┘

The high-level architecture consists of:

  1. Client Applications: Services consuming the cache

  2. Load Balancer/Proxy: Distributes requests across cache nodes

  3. Cache Cluster: A collection of cache nodes storing the actual data

  4. Configuration & Management: Handles monitoring, administration, and cluster management

Service-Specific Block Diagrams

Cache Node Internal Architecture

┌─────────────────────────────────────────────────────────────┐
│                      Cache Node                              │
│                                                             │
│  ┌─────────────┐    ┌─────────────────────────────────┐     │
│  │ Connection  │    │ Command Processor               │     │
│  │ Manager     │◄───┤ - Parser                        │     │
│  └─────────────┘    │ - Request Handler               │     │
│        │            │ - Response Formatter            │     │
│        ▼            └─────────────────┬───────────────┘     │
│  ┌─────────────┐                      │                     │
│  │ Client      │                      ▼                     │
│  │ Connections │             ┌────────────────────┐         │
│  └─────────────┘             │ Data Access Layer  │         │
│                              └────────┬───────────┘         │
│                                       │                     │
│                                       ▼                     │
│     ┌──────────────┐        ┌────────────────────┐         │
│     │ Eviction     │◄───────┤ In-Memory Store    │         │
│     │ Manager      │        │ - Hash Tables      │         │
│     └──────────────┘        │ - Data Structures  │         │
│                             └────────┬───────────┘         │
│     ┌──────────────┐                 │                     │
│     │ Expiration   │◄────────────────┘                     │
│     │ Manager      │                                       │
│     └──────────────┘                                       │
│                                                             │
│     ┌──────────────┐        ┌────────────────────┐         │
│     │ Replication  │◄───────┤ Persistence        │         │
│     │ Manager      │        │ Manager (AOF/RDB)  │         │
│     └──────────────┘        └────────────────────┘         │
└─────────────────────────────────────────────────────────────┘

Each cache node contains:

  1. Connection Manager: Handles client connections using multiplexed I/O

  2. Command Processor: Parses and executes client commands

  3. Data Access Layer: Mediates access to the actual data store

  4. In-Memory Store: Core data structures holding cached items

  5. Eviction Manager: Implements policies for removing data when memory limits are reached

  6. Expiration Manager: Tracks TTLs and removes expired entries

  7. Replication Manager: Handles data synchronization with replica nodes

  8. Persistence Manager: Manages optional durability mechanisms (AOF/RDB)

Cluster Management Service

┌───────────────────────────────────────────────────────────┐
│                Cluster Management Service                  │
│                                                           │
│  ┌────────────────┐        ┌────────────────────┐         │
│  │ Configuration  │        │ Node Registry      │         │
│  │ Store          │◄───────┤ - Node Discovery   │         │
│  │ (etcd/ZooKeeper│        │ - Health Tracking  │         │
│  └────────────────┘        └────────┬───────────┘         │
│         ▲                           │                     │
│         │                           ▼                     │
│  ┌──────┴───────┐          ┌────────────────────┐         │
│  │ Config       │          │ Shard Manager      │         │
│  │ Manager      │          │ - Hash Ring        │         │
│  └──────────────┘          │ - Rebalancing      │         │
│                            └────────┬───────────┘         │
│                                     │                     │
│  ┌────────────────┐                 ▼                     │
│  │ Failover       │        ┌────────────────────┐         │
│  │ Controller     │◄───────┤ Monitoring System  │         │
│  │                │        │ - Metrics          │         │
│  └────────────────┘        │ - Alerts           │         │
│                            └────────────────────┘         │
└───────────────────────────────────────────────────────────┘

The cluster management service includes:

  1. Configuration Store: Distributed configuration system (etcd/ZooKeeper)

  2. Node Registry: Tracks available nodes and their health

  3. Shard Manager: Handles data distribution across nodes

  4. Monitoring System: Collects performance metrics

  5. Failover Controller: Manages primary-replica transitions during failures

  6. Config Manager: Handles configuration changes and distribution

Deep Dive into Specific Components

Memory Management and Data Structures

The heart of any cache system is its memory management and data structures. For a Redis-like system, we need efficient implementations that balance speed, memory usage, and flexibility.

Key-Value Store Implementation

For our core key-value storage, we'll use a hash table with separate chaining for collision resolution. This provides O(1) average lookup time.

Why hash tables over alternatives:

  • Speed: Hash tables provide near-constant time access, significantly faster than tree-based structures (O(log n))

  • Simplicity: Simpler to implement and maintain than complex data structures like B-trees

  • Memory Efficiency: Properly sized hash tables have minimal overhead

Financial services firms like JPMorgan and Goldman Sachs prefer hash-based caching for their trading platforms due to predictable sub-millisecond access times, which is critical for real-time decision making.

Memory Allocation Strategy

For efficient memory management, we'll implement a slab allocator:

  1. Pre-allocated Slabs: Memory is divided into slabs of different sizes (64B, 128B, 256B, etc.)

  2. Size Classes: Each object is stored in the smallest slab that can fit it

  3. Slabs Management: Track free and used chunks within each slab

This approach has significant advantages over general-purpose allocators:

  • Reduced Fragmentation: Similar-sized objects are grouped together

  • Faster Allocation: Pre-allocated chunks eliminate expensive malloc/free calls

  • Better Memory Utilization: Minimizes wasted space

E-commerce platforms like Shopify and social media platforms like Instagram use slab-based allocation in their caching layers to handle billions of small objects with minimal overhead.

Eviction Policies

When memory limits are reached, the cache must decide which items to remove. We'll support multiple eviction policies:

  1. LRU (Least Recently Used):

    • Implementation: Doubly-linked list + hash map for O(1) operations

    • Use Case: General-purpose caching where recency predicts future access

  2. LFU (Least Frequently Used):

    • Implementation: Frequency counters with aging mechanism

    • Use Case: When access frequency is more important than recency

  3. Random Eviction:

    • Implementation: Simple random selection from all keys

    • Use Case: Very high throughput systems where policy computation overhead is a concern

  4. TTL-based Eviction:

    • Implementation: Min-heap ordered by expiration time

    • Use Case: Time-sensitive data where staleness is critical

The choice between these policies depends on workload characteristics:

  • Content delivery networks (CDNs) like Cloudflare typically favor LFU for static assets as popularity is often more important than recency

  • Session stores in web applications typically use LRU since recent sessions are more likely to be accessed again

  • Financial data caches often use TTL-based eviction to ensure data freshness

Persistence Mechanisms

While caches are primarily in-memory systems, persistence options provide durability and warm startup capabilities:

  1. RDB (Redis Database):

    • Point-in-time snapshots of the entire dataset

    • Advantages: Compact files, faster recovery for large datasets

    • Disadvantages: Potential data loss between snapshots

  2. AOF (Append-Only File):

    • Logs every write operation for replay during recovery

    • Advantages: Minimal data loss, easier to understand

    • Disadvantages: Larger files, slower recovery

  3. Hybrid Approach:

    • Combines periodic RDB snapshots with AOF logs

    • Advantages: Balances durability and recovery speed

    • Disadvantages: More complex implementation

E-commerce platforms typically use hybrid persistence approaches for their caching layers to balance performance with the need to preserve shopping cart data across system restarts.

Data Partitioning

As data volume grows beyond the capacity of a single node, we need strategies to distribute it across multiple machines:

Hash-Based Partitioning

The most common approach uses consistent hashing:

┌───────────────────────────────────────────────────────────┐
│                   Consistent Hash Ring                     │
│                                                           │
│    Node 1                                 Node 4          │
│      ▲                                      ▲             │
│      │                                      │             │
│      │                                      │             │
│      │                                      │             │
│      │                                      │             │
│  ────┴──────────────────────────────────────┴────────     │
│ /                                                    \    │
│ \                                                    /    │
│  ────┬──────────────────────────────────────┬────────     │
│      │                                      │             │
│      │                                      │             │
│      │                                      │             │
│      │                                      │             │
│      ▼                                      ▼             │
│    Node 2                                 Node 3          │
│                                                           │
└───────────────────────────────────────────────────────────┘

Why consistent hashing over alternative partitioning strategies:

  • Minimal Redistribution: When nodes are added or removed, only a fraction of keys need to be remapped

  • Scalability: Easily accommodates cluster expansion or contraction

  • Balance: Virtual nodes ensure even distribution despite node heterogeneity

Amazon DynamoDB's caching layer uses consistent hashing to distribute billions of items across thousands of nodes while maintaining sub-millisecond access times.

Replication Within Partitions

Each partition typically has primary and replica nodes:

┌────────────────┐      ┌────────────────┐      ┌────────────────┐
│   Partition 1  │      │   Partition 2  │      │   Partition 3  │
│  ┌──────────┐  │      │  ┌──────────┐  │      │  ┌──────────┐  │
│  │ Primary  │  │      │  │ Primary  │  │      │  │ Primary  │  │
│  └────┬─────┘  │      │  └────┬─────┘  │      │  └────┬─────┘  │
│       │        │      │       │        │      │       │        │
│       ▼        │      │       ▼        │      │       ▼        │
│  ┌──────────┐  │      │  ┌──────────┐  │      │  ┌──────────┐  │
│  │ Replica  │  │      │  │ Replica  │  │      │  │ Replica  │  │
│  └──────────┘  │      │  └──────────┘  │      │  └──────────┘  │
└────────────────┘      └────────────────┘      └────────────────┘

This approach provides:

  • High Availability: If a primary fails, a replica can be promoted

  • Read Scalability: Replicas can serve read requests

  • Disaster Recovery: Data is preserved even if nodes fail

Consistency and Replication

Cache systems must balance consistency, availability, and partition tolerance. For a Redis-like system, we'll focus on:

Replication Modes

  1. Synchronous Replication:

    • Primary waits for replica acknowledgment before responding to clients

    • Advantages: Strong consistency guarantees

    • Disadvantages: Higher latency for write operations

  2. Asynchronous Replication:

    • Primary responds immediately, replicating to secondaries in background

    • Advantages: Lower latency for write operations

    • Disadvantages: Potential data loss during failures

The financial industry often uses synchronous replication for critical caching layers despite the performance impact because data consistency is paramount. In contrast, social media platforms typically favor asynchronous replication to prioritize user experience and responsiveness.

Handling Replication Conflicts

When network partitions or node failures occur, conflicts may arise:

  1. Primary-Based Resolution:

    • Always treat the primary as the source of truth

    • Simple but can lose updates if the primary fails

  2. Vector Clocks:

    • Track causal relationships between updates

    • More complex but preserves update history

  3. Last-Writer-Wins (LWW):

    • Use timestamps to determine which update to keep

    • Simple but may lose updates

Most production cache systems like Redis implement primary-based resolution due to its simplicity and predictability, with failover mechanisms to minimize downtime during primary failures.

Identifying and Resolving Bottlenecks

Despite careful design, cache systems can develop bottlenecks:

Network Bottlenecks

Problem: High-volume traffic exhausting network bandwidth Solution:

  • Implement client-side batching to reduce request overhead

  • Use binary protocols instead of text-based ones (like RESP in Redis)

  • Deploy cache nodes in the same network segment as application servers

Memory Pressure

Problem: Running out of memory despite eviction Solution:

  • Implement data compression (LZF, Snappy)

  • Employ more aggressive TTL policies

  • Introduce sampling-based eviction for extremely large datasets

Thundering Herd

Problem: Cache misses causing database overload Solution:

  • Implement staggered expiration times

  • Use probabilistic early expiration

  • Add request coalescing for concurrent cache misses

Financial trading platforms often implement request coalescing in their cache layers to prevent database overload during market-moving events when many traders request the same data simultaneously.

Hot Keys

Problem: Few keys receiving disproportionate access Solution:

  • Implement key-specific replication factors

  • Create dedicated nodes for hot keys

  • Use local L1 caches in application servers

Security and Privacy Considerations

Cache systems often contain sensitive data and must be protected:

Authentication

  • Implement strong password authentication

  • Support for SSL/TLS encrypted connections

  • Optional LDAP or OAuth integration for enterprise deployments

Authorization

  • ACL (Access Control Lists) to restrict commands

  • Read/write separation permissions

  • Key pattern-based restrictions

Encryption

  • Encrypt data at rest for persistence files

  • Encrypt communication between nodes

  • Optional encrypted values for sensitive data

Healthcare organizations that cache patient data typically implement all these security measures to comply with regulations like HIPAA, with particular emphasis on encryption both in transit and at rest.

Monitoring and Maintenance

Effective operation requires comprehensive monitoring:

Key Metrics to Track

  • Hit Rate: Percentage of requests served from cache

  • Latency: Response time distribution

  • Memory Usage: Overall and per-data-type

  • Eviction Rate: Items being removed due to memory pressure

  • Network Traffic: Bandwidth consumption

Maintenance Operations

  • Scaling: Adding/removing nodes to accommodate changing workloads

  • Rebalancing: Redistributing data after topology changes

  • Upgrades: Performing rolling upgrades without downtime

  • Backup: Regular persistence snapshots for disaster recovery

E-commerce platforms typically increase their cache capacity by 3-5x before major shopping events like Black Friday, then scale back afterward to optimize costs.

Conclusion

Designing a cache system like Redis requires balancing many complex factors:

  • Performance vs. Durability

  • Consistency vs. Availability

  • Simplicity vs. Feature Richness

  • Memory Efficiency vs. Computational Overhead

The design decisions outlined in this article reflect the trade-offs that have proven effective in production environments for diverse workloads, from high-throughput social media platforms to latency-sensitive financial systems.

A well-designed cache system can reduce database load by 80-95%, improve application response times by an order of magnitude, and enable applications to scale to millions of users without proportional infrastructure costs.

By understanding these design principles and trade-offs, you can implement or leverage cache systems effectively in your own architectures, tailoring the approach to your specific requirements and constraints.

bottom of page