Cache Store for Clean Stack
What is Caching?
Caching is a technique used to store copies of frequently accessed data in a high-speed storage layer, allowing faster retrieval in subsequent requests. It significantly improves application performance by reducing the need to fetch data from slower storage systems or compute expensive operations repeatedly.
Why is Caching Necessary?
- Performance Improvement: Caching reduces response times and increases throughput.
- Resource Optimization: It reduces load on backend systems and databases.
- Scalability: Caching helps applications handle more concurrent users.
- Cost Reduction: By reducing computational and network load, caching can lower infrastructure costs.
Cache Store Architecture
Implementation Idea
The Cache Store in Clean Stack is designed with flexibility and efficiency in mind:
- Abstraction: The core
CacheStore
interface abstracts the caching logic, allowing different cache providers to be used interchangeably. - Provider-Agnostic: By accepting a
CacheProvider
as a parameter, the implementation supports various caching solutions (Redis, Memcached, in-memory, etc.) without changing the core logic. - Invalidation Groups: The store implements a group-based invalidation mechanism, allowing efficient invalidation of related cache entries.
- Statistics Tracking: Built-in statistics help monitor cache performance and usage.
Cache Provider as a Parameter
The CacheProvider
is passed as a parameter to the createCacheStore
function, offering several advantages:
- Flexibility: Users can choose the most suitable caching solution for their needs (Redis, Memcached, in-memory, etc.).
- Testability: It's easier to mock the cache provider in unit tests.
- Dependency Injection: This design follows the dependency injection principle, improving modularity and maintainability.
Significance of Invalidation Groups
- Efficient Bulk Invalidation: Related cache entries can be invalidated together, useful for complex data relationships.
- Fine-grained Control: Allows selective invalidation without clearing the entire cache.
- Consistency Management: Helps maintain data consistency across related cache entries. ::: Usage example:
await cacheStore.addOrReplace('user:1', userData, { groups: ['users', 'active-users'] });
// Later, invalidate all user-related caches:
await cacheStore.invalidateGroup('users');
Using the Cache Store with a REST API
To use this cache store with a REST API and Redis provider:
-
Create a Redis provider:
import { RedisClientType } from 'redis';
import { CacheProvider } from '../cache';
export function gerRedisCacheProvider(client: RedisClientType): CacheProvider {
return {
set: async (key: string, value: string, ttl?: number) => {
await client.set(key, value, { EX: ttl });
},
get: async (key: string) => {
return await client.get(key);
},
delete: async (key: string) => {
await client.del(key);
},
deleteManyKeys: async (keys: string[]) => {
await client.del(keys);
},
clear: async () => {
await client.flushAll();
},
getAllKeys: async () => {
return await client.keys('*');
},
};
} -
Set up the Redis client and cache provider:
import { createClient } from 'redis';
import { getRedisCacheProvider } from './redisCacheProvider';
import { createCacheStore } from './cacheStore';
const redisClient = createClient({ url: 'redis://localhost:6379' });
await redisClient.connect();
const redisCacheProvider = getRedisCacheProvider(redisClient);
const cacheStore = createCacheStore(redisCacheProvider); -
Use in API routes:
app.get('/users/:id', async (req, res) => {
const userId = req.params.id;
const cacheKey = `user:${userId}`;
// Try to get from cache
let userData = await cacheStore.get(cacheKey);
if (!userData) {
// If not in cache, fetch from database
userData = await fetchUserFromDatabase(userId);
// Cache the result
await cacheStore.addOrReplace(cacheKey, JSON.stringify(userData), { ttl: 3600, groups: ['users'] });
} else {
userData = JSON.parse(userData);
}
res.json(userData);
}); -
Invalidation example with group key
users
:app.post('/users', async (req, res) => {
// Create user in database
const newUser = await createUserInDatabase(req.body);
// Invalidate user-related caches
await cacheStore.invalidateGroup('users');
res.json(newUser);
});
This implementation provides a flexible, efficient, and powerful caching solution for Clean Stack, enhancing performance and scalability while maintaining ease of use and adaptability to different caching backends.