We’re sorry we missed you at re:Invent, but we can still meet!

Database caching: Momento Cache is the easiest way to cache DynamoDB

Momento delivers transparent database caching for DynamoDB where DAX falls short for developers.

Ellery Addington-White
Author

Share

I expected DynamoDB Accelerator (DAX) to be a transparent way to cache DynamoDB, making it faster and more resilient by simply changing the endpoint with minimal code changes required. Unfortunately, this ended up being far from reality. Like most developers, I would love to be able to perform experiments like adding a cache quickly (minutes) and without the price tag. 

It takes me less than a minute to make a DynamoDB table—but I find myself overwhelmed with choices when making a DAX cluster. These choices include instance type, number of replicas, zones, VPCs (if I am not already in a VPC, I have to refactor my whole stack), etc. They stand in the way of my velocity as a developer and in the way of my business delivering a better end-user experience. Needless to say, none of this sounds transparent. Database caching should be easier.

Enter Momento.

Gif of Momento entering a room.

Momento Cache is quick and easy to integrate with your stack. As you can see here, it lets you accelerate DynamoDB with just 5 lines of code. Reflecting on DAX, I wanted to make it even faster for developers to cache DynamoDB across their entire code base with just one line of code. Thanks to the power of middleware, I was able to do just that!

Mo holding DynamoDB
const db = DynamoDBDocumentClient.from(new DynamoDBClient({}));
db.middlewareStack.use(getCachingMiddleware());

I enabled a basic read-aside cache for DynamoDB (for fun, let’s call this “MAX”). To maximize the speed with which you implement DynamoDB caching, you simply add “MAX” and change the way you instantiate your JavaScript DynamoDB client. That’s it. You’re done.

Momento Cache makes it easy (and free!) to quickly experiment with this to see how much it could help you optimize your databases. Try it right now!

So how did I make it work? 

To go behind the scenes a bit, this came about after playing around with the new middleware stacks that AWS has started adding to their SDK’s like their V3 JavaScript SDK and V2 Go SDK. I found them to be incredibly powerful—but I was disappointed to find that DAX is not supported in the V3 JS SDK. Here’s a look into the hacking I did to arrive at the single-line solution to overcome this:

import {
    CacheGet,
    LogFormat,
    LogLevel,
    SimpleCacheClient,
} from '@gomomento/sdk';

const CommandCacheAllowList = [
    'GetItemCommand'
];
const cacheName = 'default';

const authToken = process.env.MOMENTO_AUTH_TOKEN;
if (!authToken) {
    throw new Error('Missing required environment variable MOMENTO_AUTH_TOKEN');
}

const defaultTtl = 300;
const momento = new SimpleCacheClient(authToken, defaultTtl, {
    loggerOptions: {
        level: LogLevel.INFO,
        format: LogFormat.JSON,
    },
});
export const getCachingMiddleware = () => {
    return {
        applyToStack: stack => {
            stack.add(
                (next) => async args => {
                    // Check if we should cache this command
                    if (CommandCacheAllowList.includes(args.constructor.name)) {
                        const itemCacheKey = getCacheKey(args);
                        if(!args.input.ConsistentRead){
                            // Check and see if we already have item in cache
                            const getResponse = await momento.get(cacheName, itemCacheKey);
                            if (getResponse instanceof CacheGet.Hit) {
                                // If item found in cache return result and skip DDB call
                                return {
                                    output: {
                                        $metadata: {},
                                        Item: JSON.parse(getResponse.valueString()),
                                    },
                                };
                            }
                        }

                        // If we didn't get cache hit let normal call path go through and then try cache result for next time
                        const result = await next(args);
                        if(result.output.Item != undefined){
                            await momento.set(
                                cacheName,
                                itemCacheKey,
                                JSON.stringify(result.output.Item)
                            );
                        }
                        return result;
                    } else {
                        return await next(args);
                    }
                },
                {tags: ['CACHE']}
            );
        },
    };
}

function getCacheKey(args) {
    return args.input.TableName + JSON.stringify(args.input.Key);
}

This also allows for a cleaner architecture, reducing the need to deploy into a VPC if using Lambda functions or deploying dedicated cache nodes that you have to pay for even when they’re idle. This approach would also be extensible to API calls to other AWS services. If you find “MAX” useful, let us know on Discord—we’ll keep building on it.

To all the JS developers out there, I hope we’ve made your new year a little brighter by giving you a path forward to cache DynamoDB—even if you’re using v3 of the AWS JavaScript SDK.

Try it out and tweet at me (@elleryaw) with your thoughts!

Share