Create Memcached Caching Module Provider

In this tutorial, you'll learn how to create a Memcached Caching Module Provider for your Medusa application.

Memcached is a high-performance, distributed memory caching system that speeds up dynamic web applications by reducing database load.

By the end of this tutorial, you'll be able to cache data in your Medusa application using Memcached.

Note: Refer to the Caching Module documentation to learn more about caching in Medusa.

Diagram illustrating the Memcached caching module provider in a Medusa application

Full Code
Find the complete code on GitHub

1. Install Memcached Client#

To interact with Memcached, you'll need to install the memjs client. You can install it in your Medusa project by running the following command:


2. Create Module Directory#

A module is created under the src/modules directory of your Medusa application. So, create the directory src/modules/memcached in your Medusa project.


3. Create Memcached Connection Loader#

Next, establish a connection to Memcached in your module using a Loader. A loader is an asynchronous function that runs when the module is initialized. It's useful for setting up connections to external services, such as databases or caching systems.

Create the file src/modules/memcached/loaders/connection.ts with the following content:

src/modules/memcached/loaders/connection.ts
1import { LoaderOptions } from "@medusajs/framework/types"2import * as memjs from "memjs"3
4export type ModuleOptions = {5  serverUrls?: string[]6  username?: string7  password?: string8  options?: memjs.ClientOptions9  cachePrefix?: string10  defaultTtl?: number // Default TTL in seconds11  compression?: {12    enabled?: boolean13    threshold?: number // Minimum size in bytes to compress14    level?: number // Compression level (1-9)15  }16}17
18export default async function connection({19  container,20  options,21}: LoaderOptions<ModuleOptions>) {22  const logger = container.resolve("logger")23  const { 24    serverUrls = ["127.0.0.1:11211"], 25    username, 26    password, 27    options: clientOptions,28  } = options || {}29
30  try {31    logger.info("Connecting to Memcached...")32
33    // Create Memcached client34    const client = memjs.Client.create(serverUrls.join(","), {35      username,36      password,37      ...clientOptions,38    })39
40    // Test the connection41    await new Promise<void>((resolve, reject) => {42      client.stats((err, stats) => {43        if (err) {44          logger.error("Failed to connect to Memcached:", err)45          reject(err)46        } else {47          logger.info("Successfully connected to Memcached")48          resolve()49        }50      })51    })52
53    // Register the client in the container54    container.register({55      memcachedClient: {56        resolve: () => client,57      },58    })59
60  } catch (error) {61    logger.error("Failed to initialize Memcached connection:", error)62    throw error63  }64}

You first define module options that are passed to the Memcached Module Provider. You'll set those up later when you register the module in your Medusa application. The module accepts the following options:

  • serverUrls: An array of Memcached server URLs. Defaults to ["127.0.0.1:11211"].
  • username: The username for authenticating with the Memcached server (if required).
  • password: The password for authenticating with the Memcached server (if required).
  • options: Additional options to pass to the Memcached client.
  • cachePrefix: A prefix to use for all cache keys to avoid collisions. Defaults to "medusa".
  • defaultTtl: The default time-to-live (TTL) for cached items, in seconds. Defaults to 3600 (1 hour).
  • compression: Configuration for data compression:
    • enabled: Whether to enable compression. Defaults to true.
    • threshold: The minimum size in bytes for data to be compressed. Defaults to 2048 (2KB).
    • level: The compression level (1-9). Defaults to 6.

Then, export a loader function. This function receives an object with the following properties:

  • container: The Module container that holds Framework and module-specific resources.
  • options: The options passed to the module when it's registered in the Medusa application.

In the loader, you create a Memcached client and test the connection. If the connection is successful, you register the client in the container, allowing you later to access it in the module's service.


4. Create Memcached Module Provider Service#

You define a module's functionalities in a service. A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can interact with third-party services to perform operations.

In this step, you'll create the service of the Memcached Module Provider. This service must implement the ICachingProviderService and implement its methods.

To create the service, create the file src/modules/memcached/service.ts with the following content:

src/modules/memcached/service.ts
1import { ICachingProviderService } from "@medusajs/framework/types"2import * as memjs from "memjs"3import { ModuleOptions } from "./loaders/connection"4
5type InjectedDependencies = {6  memcachedClient: memjs.Client7}8
9class MemcachedCachingProviderService implements ICachingProviderService {10  static identifier = "memcached-cache"11
12  protected client: memjs.Client13  protected options_: ModuleOptions14  protected readonly CACHE_PREFIX: string15  protected readonly TAG_PREFIX: string16  protected readonly OPTIONS_PREFIX: string17  protected readonly KEY_TAGS_PREFIX: string18  protected readonly TAG_KEYS_PREFIX: string19  protected readonly compressionEnabled: boolean20  protected readonly compressionThreshold: number21  protected readonly compressionLevel: number22  protected readonly defaultTtl: number23
24  constructor(25    { memcachedClient }: InjectedDependencies,26    options: ModuleOptions27  ) {28    this.client = memcachedClient29    this.options_ = options30    31    // Set all prefixes with the main prefix32    const mainPrefix = options.cachePrefix || "medusa"33    this.CACHE_PREFIX = `${mainPrefix}:`34    this.TAG_PREFIX = `${mainPrefix}:tag:`35    this.OPTIONS_PREFIX = `${mainPrefix}:opt:`36    this.KEY_TAGS_PREFIX = `${mainPrefix}:key_tags:`37    this.TAG_KEYS_PREFIX = `${mainPrefix}:tag_keys:`38    39    // Set compression options40    this.compressionEnabled = options.compression?.enabled ?? true41    this.compressionThreshold = options.compression?.threshold ?? 2048 // 2KB default42    this.compressionLevel = options.compression?.level ?? 6 // Balanced compression43    44    // Set default TTL45    this.defaultTtl = options.defaultTtl ?? 3600 // 1 hour default46  }47}

You create the service that implements the ICachingProviderService interface. You define in the class some protected properties to hold dependencies, the Memcached client, and configuration options.

You also define a constructor in the service. Service constructors accept two parameters:

  1. The Module container, from which you can resolve dependencies. In this case, you resolve the memcachedClient that you registered in the loader.
  2. The options passed to the module when it's registered in the Medusa application.

You'll get a type error at this point because you haven't implemented the methods of the ICachingProviderService interface yet. You'll implement them next, along with utility methods.

Compression Utility Methods#

Before implementing the caching methods, you'll implement two utility methods to handle data compression and decompression.

These methods use the zlib library to compress data before storing it in Memcached and decompress it when retrieving it. This optimizes storage and network usage, especially for large data.

First, add the following imports at the top of the file:

src/modules/memcached/service.ts
1import { deflate, inflate } from "zlib"2import { promisify } from "util"3
4const deflateAsync = promisify(deflate)5const inflateAsync = promisify(inflate)

Then, add the following methods to the MemcachedCachingProviderService class:

src/modules/memcached/service.ts
1class MemcachedCachingProviderService implements ICachingProviderService {2  // ...3  private async compressData(data: string): Promise<{ 4    data: string; 5    compressed: boolean6  }> {7    if (!this.compressionEnabled || data.length < this.compressionThreshold) {8      return { data, compressed: false }9    }10
11    const buffer = Buffer.from(data, "utf8")12    const compressed = await deflateAsync(buffer)13    const compressedData = compressed.toString("base64")14    15    // Only use compression if it actually reduces size16    if (compressedData.length < data.length) {17      return { data: compressedData, compressed: true }18    }19    20    return { data, compressed: false }21  }22
23  private async decompressData(24    data: string, 25    compressed: boolean26  ): Promise<string> {27    if (!compressed) {28      return data29    }30
31    const buffer = Buffer.from(data, "base64")32    const decompressed = await inflateAsync(buffer)33    return decompressed.toString("utf8")34  }35}

You define two private methods:

  • compressData: Takes a string data as input and compresses it using zlib.deflate if compression is enabled and the data size exceeds the defined threshold. It returns an object containing the (possibly compressed) data and a boolean indicating whether compression was applied.
  • decompressData: Takes a string data and a boolean compressed as input. If compressed is true, it decompresses the data using zlib.inflate. Otherwise, it returns the data as is.

Set Methods#

Next, you'll implement the set method of the ICachingProviderService interface. This method stores a value in the cache with an optional time-to-live (TTL) and associated tags.

setKeyTags Helper Method

Before implementing the method, you'll implement the helper method setKeyTags. Since Memcached doesn't support tagging natively, you'll need to manage tags manually by storing mappings between keys and tags.

Add the following method to the MemcachedCachingProviderService class:

src/modules/memcached/service.ts
1class MemcachedCachingProviderService implements ICachingProviderService {2  // ...3  private async setKeyTags(4    key: string, 5    tags: string[], 6    setOptions: memjs.InsertOptions7  ): Promise<void> {8    const timestamp = Math.floor(Date.now() / 1000)9    const tagNamespaces: Record<string, string> = {}10    const operations: Promise<any>[] = []11
12    // Batch all namespace operations13    for (const tag of tags) {14      const tagKey = this.TAG_PREFIX + tag15      const tagKeysKey = `${this.TAG_KEYS_PREFIX}${tag}`16      17      // Get namespace version18      operations.push(19        (async () => {20          const result = await this.client.get(tagKey)21          if (!result.value) {22            tagNamespaces[tag] = timestamp.toString()23            await this.client.add(tagKey, timestamp.toString())24          } else {25            tagNamespaces[tag] = result.value.toString()26          }27        })()28      )29
30      // Add key to tag's key list31      operations.push(32        (async () => {33          const result = await this.client.get(tagKeysKey)34          let keys: string[] = []35          if (result.value) {36            keys = JSON.parse(result.value.toString()) as string[]37          }38          if (!keys.includes(key)) {39            keys.push(key)40            await this.client.set(tagKeysKey, JSON.stringify(keys), setOptions)41          }42        })()43      )44    }45
46    await Promise.all(operations)47    48    // Store the tag namespaces for this key49    const keyTagsKey = `${this.KEY_TAGS_PREFIX}${key}`50    const serializedTags = JSON.stringify(tagNamespaces)51    await this.client.set(keyTagsKey, serializedTags, setOptions)52  }53}

The setKeyTags method takes a cache key, an array of tags, and Memcached setOptions.

In the method, you:

  1. Get the current timestamp to use as a namespace version for tags.
  2. Iterate over the provided tags and for each tag:
    • Retrieve the current namespace version from Memcached. If it doesn't exist, set it to the current timestamp.
    • Retrieve the list of keys associated with the tag. If the current key is not in the list, add it and update the list in Memcached.
  3. Store the mapping of tags and their namespace versions for the given key.

You'll use this method in the set method to manage tags when storing a value.

set Method

Add the following set method to the MemcachedCachingProviderService class:

src/modules/memcached/service.ts
1class MemcachedCachingProviderService implements ICachingProviderService {2  // ...3  async set({4    key,5    data,6    ttl,7    tags,8    options,9  }: {10    key: string11    data: any12    ttl?: number13    tags?: string[]14    options?: { autoInvalidate?: boolean }15  }): Promise<void> {16    const prefixedKey = this.CACHE_PREFIX + key17    const serializedData = JSON.stringify(data)18    const setOptions: memjs.InsertOptions = {}19    20    // Use provided TTL or default TTL21    setOptions.expires = ttl ?? this.defaultTtl22
23    // Compress data if enabled24    const { 25      data: finalData, 26      compressed,27    } = await this.compressData(serializedData)28
29    // Batch operations for better performance30    const operations: Promise<any>[] = [31      this.client.set(prefixedKey, finalData, setOptions),32    ]33
34    // Always store options (including compression flag) to allow checking them later35    const optionsKey = this.OPTIONS_PREFIX + key36    const optionsData = { ...options, compressed }37    operations.push(38      this.client.set(optionsKey, JSON.stringify(optionsData), setOptions)39    )40
41    // Handle tags using namespace simulation with batching42    if (tags && tags.length > 0) {43      operations.push(this.setKeyTags(key, tags, setOptions))44    }45
46    await Promise.all(operations)47  }48}

The set method takes an object with the following properties:

  • key: The cache key.
  • data: The value to cache.
  • ttl: Optional time-to-live for the cached value, in seconds.
  • tags: Optional array of tags to associate with the cached value.
  • options: Optional additional options, such as autoInvalidate, which indicates whether to automatically invalidate the cache based on tags.

In the method, you:

  1. Store the value in Memcached with the specified TTL. You store the compressed data if compression is enabled and necessary.
  2. Store the options (including whether the data was compressed) in a separate key to allow checking them later.
    • This is necessary to determine whether the data can be automatically invalidated based on tags.
  3. If tags are provided, call the setKeyTags method to set up the tag mappings.

You batch all operations using Promise.all for better performance.

Get Methods#

Next, you'll implement the get method of the ICachingProviderService interface. This method retrieves a value from the cache either by its key or by associated tags.

Before implementing the method, you need two helper methods.

validateKeyByTags Helper Method

The first helper method validates that a key is still valid based on its tags. You'll use this method when retrieving a value by its key to ensure that the value hasn't been invalidated by tag updates.

Add the following method to the MemcachedCachingProviderService class:

src/modules/memcached/service.ts
1class MemcachedCachingProviderService implements ICachingProviderService {2  // ...3  private async validateKeyByTags(key: string, tags: string[]): Promise<boolean> {4    if (!tags || tags.length === 0) {5      return true // No tags to validate6    }7
8    // Get the stored tag namespaces for this key9    const keyTagsKey = `${this.KEY_TAGS_PREFIX}${key}`10    const keyTagsResult = await this.client.get(keyTagsKey)11    12    if (!keyTagsResult.value) {13      return true // No stored tags, assume valid14    }15
16    const storedTags = JSON.parse(keyTagsResult.value.toString())17    18    // Batch all namespace checks for better performance19    const tagKeys = Object.keys(storedTags).map((tag) => this.TAG_PREFIX + tag)20    const tagResults = await Promise.all(21      tagKeys.map((tagKey) => this.client.get(tagKey))22    )23
24    // Check if any tag namespace is missing or changed25    for (let i = 0; i < tagResults.length; i++) {26      const tag = Object.keys(storedTags)[i]27      const tagResult = tagResults[i]28      29      if (tagResult.value) {30        const currrentTag = tagResult.value.toString()31        // If the namespace has changed since the key was stored, it's invalid32        if (currrentTag !== storedTags[tag]) {33          return false34        }35      } else {36        // Namespace doesn't exist - this means it was reclaimed after being incremented37        // This indicates the tag was cleared, so the key should be considered invalid38        return false39      }40    }41    42    return true43  }44}

The validateKeyByTags method accepts a cache key and an array of tags.

In the method, you:

  • Retrieve from Memcached the tag namespaces for the given key.
    • If no tags are stored, you assume the key is valid and return true.
  • For each stored tag, retrieve its current namespace version from Memcached.
  • Compare the current namespace version with the stored version. If any tag's namespace has changed or is missing, return false, indicating that the key is invalid. Otherwise, return true.

getByTags Helper Method

The second helper method retrieves the cached data associated with specified tags. You'll use this method when retrieving a value by tags.

Add the following method to the MemcachedCachingProviderService class:

src/modules/memcached/service.ts
1class MemcachedCachingProviderService implements ICachingProviderService {2  // ...3  private async getByTags(tags: string[]): Promise<any[] | null> {4    if (!tags || tags.length === 0) {5      return null6    }7
8    // Get all keys associated with each tag9    const tagKeysOperations = tags.map((tag) => {10      const tagKeysKey = `${this.TAG_KEYS_PREFIX}${tag}`11      return this.client.get(tagKeysKey)12    })13
14    const tagKeysResults = await Promise.all(tagKeysOperations)15    16    // Collect all unique keys from all tags17    const allKeys = new Set<string>()18    for (const result of tagKeysResults) {19      if (result.value) {20        const keys = JSON.parse(result.value.toString()) as string[]21        keys.forEach((key) => allKeys.add(key))22      }23    }24
25    if (allKeys.size === 0) {26      return null27    }28
29     // Get all cached data for the collected keys30     const dataOperations = Array.from(allKeys).map(async (key) => {31       const prefixedKey = this.CACHE_PREFIX + key32       const result = await this.client.get(prefixedKey)33       34       if (!result.value) {35         return { key, data: null }36       }37       38       const dataString = result.value.toString()39       40       // Check if data is compressed41       const optionsKey = this.OPTIONS_PREFIX + key42       const optionsResult = await this.client.get(optionsKey)43       let compressed = false44       45       if (optionsResult.value) {46         const options = JSON.parse(optionsResult.value.toString())47         compressed = options.compressed || false48       }49       50       // Decompress if needed51       const decompressedData = await this.decompressData(dataString, compressed)52       return { key, data: JSON.parse(decompressedData) }53     })54
55    const dataResults = await Promise.all(dataOperations)56    57    // Filter out null data and validate tags for each key58    const validData: any[] = []59    for (const { key, data } of dataResults) {60      if (data !== null) {61        // Validate that this key is still valid for the requested tags62        const isValid = await this.validateKeyByTags(key, tags)63        if (isValid) {64          validData.push(data)65        }66      }67    }68
69    return Object.keys(validData).length > 0 ? validData : null70  }71}

The getByTags method takes an array of tags.

In the method, you:

  • Retrieve from Memcached all keys associated with each tag. You collect all unique keys from the results.
  • For each unique key, retrieve the cached data with its options.
    • If the data is compressed, decompress it using the decompressData method.
  • Validate each key using the validateKeyByTags method to ensure that the data is still valid based on its tags.
  • Return an array of valid cached data or null if no valid data is found.

You can now implement the get method using these helper methods.

get Method

Add the get method to the MemcachedCachingProviderService class:

src/modules/memcached/service.ts
1class MemcachedCachingProviderService implements ICachingProviderService {2  // ...3  async get({4    key,5    tags,6  }: {7    key?: string8    tags?: string[]9  }): Promise<any> {10    if (key) {11      const prefixedKey = this.CACHE_PREFIX + key12      13      // Get the stored tags for this key and validate them14      const keyTagsKey = `${this.KEY_TAGS_PREFIX}${key}`15      const keyTagsResult = await this.client.get(keyTagsKey)16      17      if (keyTagsResult.value) {18        const storedTags = JSON.parse(keyTagsResult.value.toString())19        const tagNames = Object.keys(storedTags)20        21        const isValid = await this.validateKeyByTags(key, tagNames)22        if (!isValid) {23          return null24        }25      }26
27      const result = await this.client.get(prefixedKey)28      if (result.value) {29        const dataString = result.value.toString()30        31        // Check if data is compressed (look for compression flag in options)32        const optionsKey = this.OPTIONS_PREFIX + key33        const optionsResult = await this.client.get(optionsKey)34        let compressed = false35        36        if (optionsResult.value) {37          const options = JSON.parse(optionsResult.value.toString())38          compressed = options.compressed || false39        }40        41        // Decompress if needed42        const decompressedData = await this.decompressData(dataString, compressed)43        return JSON.parse(decompressedData)44      }45      return null46    }47
48    if (tags && tags.length > 0) {49      // Retrieve data by tags - get all keys associated with the tags50      return await this.getByTags(tags)51    }52
53    return null54  }55}

The get method takes an object with optional key and tags properties.

In the method:

  • If a key is provided, you give it a higher priority and retrieve the cached value for that key.
    • You first check if the key has associated tags and validate them using the validateKeyByTags method, ensuring the cached value is still valid.
    • You decompress the data if it was stored in a compressed format.
    • If the key is not found or is invalid, you return null. Otherwise, you return the cached value.
  • If no key is provided but tags are, you call the getByTags method to retrieve all cached values associated with the provided tags.
  • If neither key nor tags are provided, you return null.

Clear Methods#

Finally, you'll implement the clear method of the ICachingProviderService interface.

The clear method removes a cached value either by its key or by associated tags. It also receives an optional options parameter to control whether to automatically invalidate the cache based on tags:

  • If options isn't set, you clear all cached values associated with the provided tags.
  • If options.autoInvalidate is true, you only invalidate the keys of the provided tags whose options allow automatic invalidation.

Before implementing the clear method, you'll implement four helper methods to handle tag and key invalidation.

removeKeysFromTag Helper Method

The removeKeysFromTag method removes a list of keys from a tag's key list in Memcached. This is useful when invalidating by key or when clearing keys associated with a tag.

Add the following method to the MemcachedCachingProviderService class:

src/modules/memcached/service.ts
1class MemcachedCachingProviderService implements ICachingProviderService {2  // ...3  private async removeKeysFromTag(tag: string, keysToRemove: string[]): Promise<void> {4    const tagKeysKey = `${this.TAG_KEYS_PREFIX}${tag}`5    const tagKeysResult = await this.client.get(tagKeysKey)6    7    if (!tagKeysResult.value) {8      return // No keys to remove9    }10    11    let keys: string[] = JSON.parse(tagKeysResult.value.toString()) as string[]12    13    // Remove the specified keys14    keys = keys.filter((key) => !keysToRemove.includes(key))15    16    if (keys.length === 0) {17      // If no keys left, delete the tag keys entry18      await this.client.delete(tagKeysKey)19    } else {20      // Update the tag keys list21      await this.client.set(tagKeysKey, JSON.stringify(keys))22    }23  }24}

The removeKeysFromTag method takes a tag and an array of keysToRemove.

In the method, you:

  1. Retrieve the list of keys associated with the tag from Memcached.
  2. Filter out the keys that need to be removed.
  3. If no keys are left, delete the tag's key list entry. Otherwise, update the list in Memcached.

clearByKey Helper Method

Next, you'll implement the clearByKey method. It removes a cached value by its key and updates the associated tags to remove the key from their key lists.

Add the following method to the MemcachedCachingProviderService class:

src/modules/memcached/service.ts
1class MemcachedCachingProviderService implements ICachingProviderService {2  // ...3  private async clearByKey(key: string): Promise<void> {4    // Get the key's tags before deleting to clean up tag key lists5    const keyTagsKey = `${this.KEY_TAGS_PREFIX}${key}`6    const keyTagsResult = await this.client.get(keyTagsKey)7    8    const operations: Promise<any>[] = [9      this.client.delete(this.CACHE_PREFIX + key),10      this.client.delete(this.OPTIONS_PREFIX + key),11      this.client.delete(keyTagsKey),12    ]13
14    // If the key has tags, remove it from tag key lists15    if (keyTagsResult.value) {16      const storedTags = JSON.parse(keyTagsResult.value.toString())17      const tagNames = Object.keys(storedTags)18      19      // Batch tag cleanup operations20      const tagCleanupOperations = tagNames.map(async (tag) => {21        await this.removeKeysFromTag(tag, [key])22      })23      operations.push(...tagCleanupOperations)24    }25
26    await Promise.all(operations)27  }28}

The clearByKey method takes a cache key.

In the method, you:

  1. Retrieve the tags associated with the key before deleting it.
  2. Delete the cached value, its options, and the key's tag mapping from Memcached.
  3. If the key has associated tags, call the removeKeysFromTag method for each tag to remove the key from their key lists.
  4. Batch all operations using Promise.all for better performance.

clearByTags Helper Method

Next, you'll implement the clearByTags method. It removes all cached values associated with the provided tags. You'll use this method when the options parameter isn't set in the clear method.

Add the following method to the MemcachedCachingProviderService class:

src/modules/memcached/service.ts
1class MemcachedCachingProviderService implements ICachingProviderService {2  // ...3  private async clearByTags(tags: string[]): Promise<void> {4    const operations = tags.map(async (tag) => {5      const tagKey = this.TAG_PREFIX + tag6      const result = await this.client.increment(tagKey, 1)7      if (result === null) {8        // Key doesn't exist, create it with current timestamp9        const timestamp = Math.floor(Date.now() / 1000)10        await this.client.add(tagKey, timestamp.toString())11      }12    })13
14    await Promise.all(operations)15  }16}

The clearByTags method takes an array of tags.

In the method, you loop over the tags to increment their namespace versions in Memcached. If a tag's namespace doesn't exist, you create it with the current timestamp.

By incrementing the namespace version, you effectively invalidate the tag and all associated keys, as they will no longer match the stored namespace versions. The namespace version will also be replaced in Memcached after being reclaimed.

Tip: Learn more about this invalidation strategy in Memcached's documentation.

clearByTagsWithAutoInvalidate Helper Method

The clearByTagsWithAutoInvalidate method removes cached values associated with the provided tags, but only for keys whose options allow automatic invalidation. You'll use this method when the options.autoInvalidate parameter is true in the clear method.

Add the following method to the MemcachedCachingProviderService class:

src/modules/memcached/service.ts
1class MemcachedCachingProviderService implements ICachingProviderService {2  // ...3  private async clearByTagsWithAutoInvalidate(tags: string[]): Promise<void> {4    for (const tag of tags) {5      // Get the list of keys associated with this tag6      const tagKeysKey = `${this.TAG_KEYS_PREFIX}${tag}`7      const tagKeysResult = await this.client.get(tagKeysKey)8      9      if (!tagKeysResult.value) {10        continue11      }12      13      const keys = JSON.parse(tagKeysResult.value.toString()) as string[]14      15      // Check each key's options and delete if autoInvalidate is true16      const keysToRemove: string[] = []17      for (const key of keys) {18        const optionsKey = `${this.OPTIONS_PREFIX}${key}`19        const optionsResult = await this.client.get(optionsKey)20        21        if (optionsResult.value) {22          const options = JSON.parse(optionsResult.value.toString())23          if (options.autoInvalidate) {24            // Delete the key and its associated data25            await this.client.delete(this.CACHE_PREFIX + key)26            await this.client.delete(optionsKey)27            await this.client.delete(`${this.KEY_TAGS_PREFIX}${key}`)28            keysToRemove.push(key)29          }30        }31      }32      33      // Remove deleted keys from the tag's key list34      if (keysToRemove.length > 0) {35        await this.removeKeysFromTag(tag, keysToRemove)36      }37    }38  }39}

You define the clearByTagsWithAutoInvalidate method, which takes an array of tags.

In the method, you loop over the tags to:

  1. Retrieve the list of keys associated with each tag from Memcached.
  2. For each key, retrieve its options and check if autoInvalidate is true.
  3. If autoInvalidate is true, delete the cached value, its options, and its tag mapping from Memcached. You also keep track of the keys that were deleted.
  4. After processing all keys for a tag, call the removeKeysFromTag method to remove the deleted keys from the tag's key list.

clear Method

Finally, add the clear method to the MemcachedCachingProviderService class:

src/modules/memcached/service.ts
1class MemcachedCachingProviderService implements ICachingProviderService {2  // ...3  async clear({4    key,5    tags,6    options,7  }: {8    key?: string9    tags?: string[]10    options?: { autoInvalidate?: boolean }11  }): Promise<void> {12    if (key) {13      await this.clearByKey(key)14    }15
16    if (tags?.length) {17      if (!options) {18        // Clear all items with the specified tags19        await this.clearByTags(tags)20      } else if (options.autoInvalidate) {21        // Clear only items with autoInvalidate option set to true22        await this.clearByTagsWithAutoInvalidate(tags)23      }24    }25  }26}

The clear method takes an object with optional key, tags, and options properties.

In the method:

  • If a key is provided, you call the clearByKey method to remove the cached value and update associated tags.
  • If tags are provided:
    • If options isn't set, you call the clearByTags method to invalidate all cached values associated with the tags.
    • If options.autoInvalidate is true, you call the clearByTagsWithAutoInvalidate method to invalidate only the keys whose options allow automatic invalidation.

You've now implemented all methods of the ICachingProviderService interface in the MemcachedCachingProviderService class.


5. Export Memcached Module Provider Definition#

The final piece of a module provider is its definition, which you export in an index.ts file at its root directory. This definition tells Medusa which module this provider belongs to, its loaders, and its service.

Create the file src/modules/memcached/index.ts with the following content:

src/modules/memcached/index.ts
1import { ModuleProvider, Modules } from "@medusajs/framework/utils"2import MemcachedCachingProviderService from "./service"3import connection from "./loaders/connection"4
5export default ModuleProvider(Modules.CACHING, {6  services: [MemcachedCachingProviderService],7  loaders: [connection],8})

You use the ModuleProvider function from the Modules SDK to create the module provider's definition. It accepts two parameters:

  1. The module this provider belongs to. In this case, the Modules.CACHING module.
  2. An object with the provider's services and loaders.

6. Register Memcached Module Provider#

The last step is to register the Memcached Module Provider in your Medusa application.

Enable Caching Feature Flag#

First, enable the Caching Module's feature flag by setting the following environment variable:

Terminal
MEDUSA_FF_CACHING=true

Register Memcached Module Provider#

Then, in medusa-config.ts, add a new entry in the modules array to register the Memcached Module Provider:

medusa-config.ts
1module.exports = defineConfig({2  // ...3  modules: [4    {5      resolve: "@medusajs/medusa/caching",6      options: {7        in_memory: {8          enable: true,9        },10        providers: [11          {12            resolve: "./src/modules/memcached",13            id: "caching-memcached",14            // Optional, makes this the default caching provider15            is_default: true,16            options: {17              serverUrls: process.env.MEMCACHED_SERVERS?.split(",") || 18                ["127.0.0.1:11211"],19              // add other optional options here...20            },21          },22          // other caching providers...23        ],24      },25    },26  ],27})

You register the @medusajs/medusa/caching module and add the Memcached Module Provider to its providers array.

You pass the options to configure the Memcached client and the module's behavior. These are the same options you defined in the ModuleOptions type in the loader.

Add Environment Variables#

Make sure you set the necessary environment variables in your .env file. For example:

Terminal
MEMCACHED_SERVERS=127.0.0.1:11211 # Comma-separated list of Memcached server URLs# Add other optional variables as needed

You set the MEMCACHED_SERVERS variable to specify the Memcached server URLs. You can also set other optional variables like MEMCACHED_USERNAME and MEMCACHED_PASSWORD based on your use case.


Test the Memcached Caching Provider#

To test that the Memcached Caching Provider is working, start the Medusa application with the following command:

You'll see in the logs that the Memcached connection is established successfully:

Terminal
info:    Connecting to Memcached...info:    Successfully connected to Memcached

If you set the is_default option to true in the provider registration, the Memcached Caching Provider will be used for all caching operations in the Medusa application.

Create Test API Route#

To verify that the caching is working, you can create a simple API route that retrieves data with caching.

To create an API route, create a new file at src/api/test-cache/route.ts with the following content:

src/api/test-cache/route.ts
1import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"2
3export const GET = async (req: MedusaRequest, res: MedusaResponse) => {4  const query = req.scope.resolve("query")5    6  // Test caching with a simple query7  const { data } = await query.graph({8    entity: "product",9    fields: ["id", "title", "handle"],10  }, {11    cache: {12      enable: true,13      // For testing purposes14      key: "test-cache-products",15      // If you didn't set is_default to true, uncomment the following line16      // providers: ["caching-memcached"],17    },18  })19
20  res.status(200).json({21    message: "Products retrieved with Memcached caching",22    data,23  })24}

This creates a GET route at /test-cache that retrieves products using Query with caching enabled. The key so that you can easily check the cached data in Memcached for testing purposes.

Then, send a GET request to the /test-cache endpoint:

Code
curl http://localhost:9000/test-cache

You'll receive the list of products in the response.

You can then check that the data is cached in Memcached using the memcached-cli tool.

First, establish a connection to your Memcached server:

Terminal
npx memcached-cli localhost:11211 # Replace with your Memcached server URL

Then, retrieve the cached data using the key you specified in the API route:

Terminal
get medusa:test-cache-products

Notice that you prefix the key with medusa:, which is the default prefix unless you set the keyPrefix option in the provider registration.


Next Steps#

If you're new to Medusa, check out the main documentation, where you'll get a more in-depth understanding of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's Commerce Modules.

Troubleshooting#

If you encounter issues during your development, check out the troubleshooting guides.

Getting Help#

If you encounter issues not covered in the troubleshooting guides:

  1. Visit the Medusa GitHub repository to report issues or ask questions.
  2. Join the Medusa Discord community for real-time support from community members.
Was this page helpful?
Ask Anything
FAQ
What is Medusa?
How can I create a module?
How can I create a data model?
How do I create a workflow?
How can I extend a data model in the Product Module?
Recipes
How do I build a marketplace with Medusa?
How do I build digital products with Medusa?
How do I build subscription-based purchases with Medusa?
What other recipes are available in the Medusa documentation?
Chat is cleared on refresh
Line break