Based on the needs of your Node.js application and your deployment infrastructure you might need to use internal or external caching. Internal cache stores objects inside your Node.js app, and they are available only to it. While external cache is deployed as a separate instance and available to any Node.js app and used when scaling. Redis and Memcached are the most popular choices as external caching systems. Internal and External cache can be used together. Consider all tradeoffs when making decisions.
In this part, we’ll review one of the modules for internal caching, node-cache. First, let’s review caching techniques.
Write-through — writes both to a cache and to a database, and only when both are completed, returns the competition result to a user. It doesn’t really matter if it writes in parallel or sequentially, synchronously or asynchronously (always do asynchronously for Node.js); the main principle if return competition state only after it is written to the cache and the DB.
Write-behind — writes to a cache, and when it is completed, returns the competition result to a user. It writes to a database in background, and doesn’t wait when it is finished.
Write-around — writes to a database only, doesn’t store to a cache. It writes to the cache on a first read from the database.
Choose an appropriate technique based on your use cases and infrastructure.
node-cache as an Internal Cache
$ git clone https://github.com/losikov/api-example.git
$ cd api-example
$ git checkout tags/v6.1.0
$ yarn install
Install node-cache npm:
$ yarn add node-cache
node-cache has global TTL, time to leave for the objects it stores. It also supports it per each key/object. Let’s define the global one in the config. Add the definition to config/.env.schema file:
and, to config/.env.defaults, the value:
Update src/config/index.ts file. Add localCacheTtl to Config interface, and to config constant:
Finally, let’s define our cache as a singleton. Create src/utils/cache_local.ts file with the following content:
We could implement get and set as promises, but it is an overhead for an internal cache.
Just for an example, let’s use write-around technique: write to the cache only when it reads the value from the DB for the first time. Update src/api/services/greeting.ts to the following content:
As u see, we read the value from cache in line 7, and if the value doesn’t exist we read it from the database (line 9) and store to the cache in line 14.
Do the same for login function in src/api/services/user.ts:
We store user object in the cache twice (lines 19–20) under different keys.
To test the performance improvement I’ve added the following code to src/api/controllers/__tests__/greeting.ts:
If you run unit tests using inmemory MongoDB, you won’t notice see the difference. But you can easily make unit tests work with real MongoDB. Run it with ./scripts/run_dev_dbs.sh -r, and modify config/.env.test file:
MONGO_URL=mongodb://<localhost or your IP address>/exmpl
In my case, the number of GET /api/v1/goodbye requests processed per second by this test increased from ~280 to ~820. For inmemory db used in unit tests by default, it doesn’t change as expected.
don’t consider 820 as server performance measured in requests processed per second, as it is just unit test running requests sequentially. We’ll get to the performance measuring in the next parts.
You can download the project sources from git https://github.com/losikov/api-example. Git commit history and tags are organized based on the parts.