Cloud

Serverless Performance: Designing a Distributed Cache with DynamoDB

Implementing a cost-effective, high-performance caching strategy using DynamoDB and TTL for high-traffic Node.js applications.

November 15, 2024
3 min read
Serverless Performance: Designing a Distributed Cache with DynamoDB

Introduction

When thinking of caching, Redis is usually the default answer. However, in a recent high-traffic project involving insurance policy data, infrastructure complexity and cost management were top priorities. I needed a solution that was serverless, practically infinite in scale, and required zero maintenance.

Enter Amazon DynamoDB as a distributed cache.

Why DynamoDB?

Redis is powerful, but managing a cluster (or paying for a managed one) adds overhead. For our use case—caching API responses and complex query results—DynamoDB offered specific advantages:

  1. Serverless: No instances to provision or patch.
  2. TTL (Time To Live): Native support for automatic item expiry.
  3. Millisecond Latency: Consistent performance at any scale.

The Implementation

I designed a generic CacheService within our NestJS backend that abstracted the underlying storage mechanism. This allowed us to easily swap storage providers, but the primary implementation used DynamoDB.

1. Table Design for Caching

The table design was simple but effective:

  • Partition Key (PK): The cache key (e.g., user:123:profile or query:products:active).
  • Attribute (value): The serialized JSON data.
  • Attribute (expiresAt): A UNIX timestamp used by DynamoDB’s TTL feature.
sequenceDiagram
    participant Client
    participant API
    participant Cache as DynamoDB Cache
    participant DB as Primary DB
    
    Client->>API: Request data
    API->>Cache: Check cache (getItem)
    
    alt Cache Hit & Not Expired
        Cache-->>API: Return cached data
        API-->>Client: Response (fast)
    else Cache Miss or Expired
        Cache-->>API: No data / expired
        API->>DB: Fetch fresh data
        DB-->>API: Return data
        API->>Cache: Update cache (async)
        API-->>Client: Response
    end
    
    Note over Cache: TTL auto-deletes expired items

2. The Abstract Service

I built the service to handle the “stale-while-revalidate” pattern and standard lookups.

// Simplified logic of the Cache Service
async getOrSet<T>(key: string, ttlSeconds: number, fetcher: () => Promise<T>): Promise<T> {
  const cachedItem = await this.dynamoDbClient.getItem({ Key: { pk: key } });

  if (cachedItem && cachedItem.expiresAt > Date.now()) {
    return JSON.parse(cachedItem.value);
  }

  // Cache miss or expired: Fetch fresh data
  const freshData = await fetcher();
  
  // Asynchronously update cache to not block response
  this.set(key, freshData, ttlSeconds);
  
  return freshData;
}

Solving the “Zero Downtime” Migration

One of the more complex aspects of this project was synchronizing data between the transactional document store (MongoDB/DocumentDB) and this caching layer. I implemented specific event triggers that would invalidate or update specific cache keys whenever the master data changed, ensuring users never saw stale data for critical operations.

graph LR
    subgraph "Data Flow"
        App["Application"]
        Primary[("Primary DB<br/>MongoDB")]
        Events["Change Events"]
        Lambda["Cache Invalidator<br/>Lambda"]
        Cache[("DynamoDB Cache")]
    end
    
    App -->|write| Primary
    Primary -.->|trigger| Events
    Events -->|invoke| Lambda
    Lambda -->|invalidate/update| Cache
    App -->|read| Cache
    
    style Primary fill:#51cf66,stroke:#2f9e44
    style Cache fill:#74c0fc,stroke:#1c7ed6
    style Lambda fill:#ffa94d,stroke:#d9480f

Results

By offloading repetitive read operations to DynamoDB:

  • Latency Drop: Average response times for complex dashboard endpoints dropped from ~400ms to <50ms.
  • Database Load: Read throughput on the primary PostgreSQL database decreased by 40%.
  • Cost: significantly lower operational cost compared to a provisioned Redis cluster for our specific traffic pattern.

Conclusion

Redis isn’t always the only tool for the job. By leveraging DynamoDB’s TTL features, I built a robust, maintenance-free caching layer that scaled perfectly with the serverless architecture.

Tags

#AWS #DynamoDB #Performance #Caching #Node.js
Rommel

Written by Rommel Saquicela

Senior Tech Lead & Solutions Architect

View Profile