Modularizing the Cache Service: A Practical Guide to OOP in Redis
Stop scattering Redis logic throughout your codebase. Learn how to implement a reusable Cache class to encapsulate your data operations and improve testability.

Previously in this course, we explored error handling in Redis clients to ensure our applications remain resilient under pressure. While our error handling is now robust, our Redis logic is likely still peppered throughout our controllers and routes, leading to tight coupling and "spaghetti code."
In this lesson, we will apply Modularization to transition from procedural, ad-hoc Redis commands to a centralized CacheService. By wrapping our Redis interactions in an OOP structure, we make our code easier to maintain, test, and swap out if our infrastructure needs change.
Why Modularize Your Cache Logic?
In a small project, calling redis.get() directly in your route handlers seems fine. However, as your application grows, this creates several "code smells":
- Duplication: You find yourself rewriting the same logic for key prefixing, JSON serialization, and TTL handling in every file.
- Harder Testing: Testing a route that is tightly coupled to a global Redis instance is difficult. You want to mock the cache interface, not the database driver itself.
- Lack of Abstraction: If you decide to switch from Redis to an in-memory store like Memcached, you would have to search and replace your entire codebase.
A service layer acts as a gatekeeper, providing a clean interface that your application can talk to without needing to know the implementation details of your storage backend.
Creating a Cache Class: A Worked Example

We will build a CacheService that handles the heavy lifting of serialization and key management. We'll use the ioredis or node-redis pattern.
JAVASCRIPT// src/services/CacheService.js class CacheService { constructor(client, defaultTtl = 3600) { this.client = client; this.defaultTtl = defaultTtl; } async get(key) { const data = await this.client.get(key); return data ? JSON.parse(data) : null; } async set(key, value, ttl = this.defaultTtl) { const serialized = JSON.stringify(value); return await this.client.set(key, serialized, CE9178">'EX', ttl); } async delete(key) { return await this.client.del(key); } } module.exports = CacheService;
Dependency Injection
Instead of creating a new Redis connection inside the class, we pass it in via the constructor. This is a crucial concept in refactoring monolithic components. It allows you to inject a "mock" client during unit tests, ensuring your tests don't actually require a running Redis server.
Integrating the Service into the Project
Now, let's update our project baseline (established in setting up the backend project baseline) to use this new structure.
JAVASCRIPT// src/app.js const redis = require(CE9178">'redis'); const CacheService = require(CE9178">'./services/CacheService'); const client = redis.createClient(); const cache = new CacheService(client); // Usage in a route app.get(CE9178">'/api/user/:id', async (req, res) => { const cacheKey = CE9178">`user:${req.params.id}`; // Clean, readable cache access const cachedUser = await cache.get(cacheKey); if (cachedUser) return res.json(cachedUser); const user = await db.fetchUser(req.params.id); await cache.set(cacheKey, user); res.json(user); });
Hands-on Exercise
- Refactor: Take your existing rate-limiting logic from implementing a basic rate limiter.
- Encapsulate: Add a
incrementCountermethod to yourCacheServicethat handles the specific key-formatting and TTL logic. - Verify: Update one of your route handlers to call
cache.incrementCounter()instead of the rawredis.incr()command.
Common Pitfalls

- Over-Engineering: Don't turn every single Redis command into a class method. Only abstract the operations you perform repeatedly (e.g., getters, setters, and specialized incrementers).
- Forgetting Serialization: When using a class, it's tempting to store complex objects. Always ensure your service handles
JSON.stringifyandJSON.parseconsistently to prevent[object Object]from being saved in Redis. - Leaking Implementation: If your service returns raw Redis promises or client-specific error objects, you’ve broken the abstraction. Return standard values or domain-specific errors.
FAQ
Q: Does using a class impact performance? A: Negligible. The overhead of a JavaScript class method call is nanoseconds compared to the network latency of a Redis request.
Q: What if I need different TTLs for different data?
A: Notice our set method accepts an optional ttl parameter with a default value. This allows for flexibility while maintaining a clean default behavior.
Recap

By modularizing your cache logic, you’ve moved from scattered, fragile code to a robust service-oriented architecture. You've encapsulated your Redis logic, enabled dependency injection for easier testing, and created a standard interface for your team to use. This is a foundational step in refactoring with confidence as your application grows in complexity.
Up next: We will discuss Securing Redis Access to ensure your production cache is protected from unauthorized connections.


