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.
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:
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 to3600
(1 hour).compression
: Configuration for data compression:enabled
: Whether to enable compression. Defaults totrue
.threshold
: The minimum size in bytes for data to be compressed. Defaults to2048
(2KB).level
: The compression level (1-9). Defaults to6
.
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:
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:
- The Module container, from which you can resolve dependencies. In this case, you resolve the
memcachedClient
that you registered in the loader. - 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:
Then, add the following methods to the MemcachedCachingProviderService
class:
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 stringdata
as input and compresses it usingzlib.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 stringdata
and a booleancompressed
as input. Ifcompressed
istrue
, it decompresses the data usingzlib.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:
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:
- Get the current timestamp to use as a namespace version for tags.
- 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.
- 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:
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 asautoInvalidate
, which indicates whether to automatically invalidate the cache based on tags.
In the method, you:
- Store the value in Memcached with the specified TTL. You store the compressed data if compression is enabled and necessary.
- 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.
- 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:
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
.
- If no tags are stored, you assume the key is valid and return
- 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, returntrue
.
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:
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.
- If the data is compressed, decompress it using the
- 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:
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.
- You first check if the key has associated tags and validate them using the
- If no
key
is provided buttags
are, you call thegetByTags
method to retrieve all cached values associated with the provided tags. - If neither
key
nortags
are provided, you returnnull
.
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
istrue
, 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:
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:
- Retrieve the list of keys associated with the tag from Memcached.
- Filter out the keys that need to be removed.
- 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:
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:
- Retrieve the tags associated with the key before deleting it.
- Delete the cached value, its options, and the key's tag mapping from Memcached.
- If the key has associated tags, call the
removeKeysFromTag
method for each tag to remove the key from their key lists. - 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:
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.
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:
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:
- Retrieve the list of keys associated with each tag from Memcached.
- For each key, retrieve its options and check if
autoInvalidate
istrue
. - If
autoInvalidate
istrue
, delete the cached value, its options, and its tag mapping from Memcached. You also keep track of the keys that were deleted. - 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:
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 theclearByKey
method to remove the cached value and update associated tags. - If
tags
are provided:- If
options
isn't set, you call theclearByTags
method to invalidate all cached values associated with the tags. - If
options.autoInvalidate
istrue
, you call theclearByTagsWithAutoInvalidate
method to invalidate only the keys whose options allow automatic invalidation.
- If
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:
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:
- The module this provider belongs to. In this case, the
Modules.CACHING
module. - An object with the provider's
services
andloaders
.
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:
Register Memcached Module Provider#
Then, in medusa-config.ts
, add a new entry in the modules
array to register the Memcached Module Provider:
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:
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:
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:
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:
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:
Then, retrieve the cached data using the key you specified in the API route:
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:
- Visit the Medusa GitHub repository to report issues or ask questions.
- Join the Medusa Discord community for real-time support from community members.