Building E-Commerce Applications with Dragonfly
Tags: redis, database
Published: 2023-11-21 17:00:00

## Introduction

In the high-octane world of e-commerce applications, both response speed and data accuracy are crucial.
Customers expect seamless access to searched items, past orders, recently viewed products, and personalized recommendations.
In the meantime, these applications often experience fluctuating traffic, especially during peak periods like the Christmas season or Black Friday.
High-traffic events furthermore introduce significant challenges, requiring rapid response and precise data handling.
Addressing these variations often demands a scalable and robust in-memory data storage solution.

Dragonfly, an ultra-performant in-memory data store, utilizes a multi-threaded, shared-nothing architecture that pushes hardware to its limits,
supporting up to 4 million ops/sec and 1 TB of memory on a single instance.
This can drastically reduce operational complexities while providing a high-performance solution for e-commerce applications.
For even more demanding scenarios, Dragonfly also offers cluster mode on top of the stunning single-node performance.
This adaptability makes Dragonfly an ideal choice for e-commerce platforms that contend with unpredictable and varied traffic patterns.

In this blog, we will explore how Dragonfly can be used in various ways to elevate your e-commerce platform's performance and user experience, particularly in the following areas:

- [**Caching**](#caching) - **String**, **Hash**, and **JSON** data types are ideal for caching.
- [**Personalization**](#personalization) - **Sorted-Set** is perfect for tracking user preferences.
- [**High-Traffic Flash Sales**](#high-traffic-flash-sales) - **Atomic Operations** and **Distributed Locks** can be used to manage inventory verification and deduction under extremely demanding situations.

---

## Caching

Caching is a powerful strategy in web technology, particularly important for e-commerce applications.
It enables quicker data retrieval by storing the results of a database query or an API request in a cache.
When an identical request is made, the data can be swiftly served from the cache (in this case, Dragonfly) avoiding the need for time-consuming interactions with the primary database.

Selecting the appropriate data type for caching is crucial to easing the implementation and optimizing the performance of your e-commerce platform.
The most accessible data type is a `String`, ideal for caching a blob of data.
It's versatile and safe to store various formats, whether text or binary, like JSON strings, MessagePacks, or Protocol Buffers.
For instance, a user's recent order summary could be cached as a JSON string.
However, the downside of using the `String` data type is the difficulty in manipulating individual fields within cached data.

```shell
# Using the 'String' data type for caching.

# To cache an order:
dragonfly$> SET order_string:<order_id> '{"id": "<order_id>", "items": [{"id": "001", "name": "Laptop", "quantity": 1}], "total": 1799.99}'

# To retrieve the entire cached order:
dragonfly$> GET order_string:<order_id>
```

Alternatively, a `Hash` data type, which is a single-level string-to-string flat hashmap, is suitable for storing field/value pairs.
This can be used to cache specific attributes of a user's order, like item IDs and quantities, allowing for quicker access and updates.

```shell
# Using the 'Hash' data type for caching.

# To cache quantities for different items and the total price in the order:
dragonfly$> HSET order_hash:<order_id> item_001 5 item_002 6 item_003 7 total 2799.99

# To retrieve the quantity of a specific item:
dragonfly$> HGET order_hash:<order_id> item_003

# To retrieve the entire cached order:
dragonfly$> HGETALL order_hash:<order_id>
```

Lastly, for more complex data structures, the `JSON` data type in Dragonfly natively and fully supports the [JSON](https://www.json.org/json-en.html) specification
and the [JSONPath](https://github.com/json-path/JsonPath) syntax, enabling easy manipulation of individual fields.
This is particularly useful for detailed order information, where each aspect of an order (such as product details, pricing, and shipping info)
can be individually accessed and modified, providing both flexibility and efficiency in data handling.

```shell
# Using the 'JSON' data type for caching.

# To cache an order as native JSON:
dragonfly$> JSON.SET order_json:<order_id> $ '{"id": "<order_id>", "items": [{"id": "001", "name": "Laptop", "quantity": 1}], "total": 1799.99}'

# To update the quantity of a specific item:
dragonfly$> JSON.SET order_json:<order_id> $.items[0].quantity 2

# To retrieve the total price of the order:
dragonfly$> JSON.GET order_json:<order_id> $.total

# To retrieve the entire cached order:
dragonfly$> JSON.GET order_json:<order_id>
```

For more caching-related techniques, check out our previous blog posts:

- [Developing with Dragonfly: Cache-Aside](https://www.dragonflydb.io/blog/developing-with-dragonfly-part-01-cache-aside) to follow along with a step-by-step tutorial on how to implement a cache-aside pattern using Dragonfly.
- [Developing with Dragonfly: Solve Caching Problems](https://www.dragonflydb.io/blog/developing-with-dragonfly-part-02-solve-caching-problems) to learn how to solve the 3 common caching problems (Penetration, Breakdown, and Avalanche) with Dragonfly.
- [Dragonfly Cache Design](https://www.dragonflydb.io/blog/dragonfly-cache-design) to learn more about the internal eviction algorithm of Dragonfly.

---

## Personalization

In e-commerce applications, personalizing the user experience is key.
One effective way to achieve this is by showcasing prioritized items, such as the most-viewed product categories for a particular user or the top items viewed globally on the application for the day.
`Sorted-Set`, a data structure available in Dragonfly, is perfectly suited for this task.
It's a collection of unique elements, each associated with a score, which determines the order of the elements.

For instance, to track a user's most-viewed product categories, we can use a `Sorted-Set` where each category is a member and the number of times the user views that category is the score.
Every time a user views a category, the score is incremented, ensuring the set always reflects the user's current preferences.

```shell
# Using the 'Sorted-Set' data type to track user preferences.

# To increment the view count for a category for a user:
dragonfly$> ZINCRBY viewed_product_categories_by_user_id:<user_id> 1 "electronics"

# To retrieve the top 5 viewed categories for a user:
dragonfly$> ZREVRANGE viewed_product_categories_by_user_id:<user_id> 0 4 WITHSCORES
```

Similarly, for global views, we can maintain a `Sorted-Set` for the entire application, where each view of a product category by any user increments the category's score.
This approach allows for dynamic, real-time ranking of categories or items based on popularity, providing valuable insights for both users and the platform.

`Sorted-Set` an important data structure in many applications, particularly for scenarios like those mentioned above.
Redis has long been celebrated for its robust implementation of this data structure, facilitating efficient data sorting and retrieval.
However, starting from v1.11, Dragonfly introduces a [B+ Tree](https://en.wikipedia.org/wiki/B%2B_tree)-based implementation.
This new implementation not only enhances performance but also improves memory efficiency in terms of size, making it an excellent choice for handling large-scale data sorting and ranking tasks.
We plan to explore this topic in greater depth in a future blog post.

---

## High-Traffic Flash Sales

In our earlier discussion, we highlighted the challenges e-commerce platforms face with fluctuating traffic, particularly during high-profile events like Black Friday flash sales.
During these peak periods, Dragonfly can play a pivotal role, especially in managing inventory verification and deduction.
While these tasks can be performed using a traditional SQL database, the simultaneous attempts by numerous users to purchase limited-stock items can quickly overwhelm the primary database.
This is where the capabilities of Dragonfly, as an ultra-performant in-memory data store become invaluable.

Dragonfly can address this challenge with two mechanisms: **Atomic Operations** and **Distributed Locks**.
Each mechanism can in turn be implemented in different ways, which we will explore in detail below.

### 1. Atomic Operations with `INCR` or `DECR`

Dragonfly's atomicity ensures that each command, such as incrementing or decrementing a value, is executed entirely and independently, without interference from other operations.
Consider a scenario where we have a limited stock of a product for a flash sale, we can initialize the inventory count in Dragonfly by setting the quantity:

```shell
# Assuming the flash sale has 100 units for a particular item.
dragonfly$> SET item_on_sale:<item_id> 100
```

For simplicity, we assume that each request is for a single unit of the item.
When a purchase request is made, we use the `DECR` command to deduct inventory atomically:

```shell
dragonfly$> DECR item_on_sale:<item_id>
```

The return value of the `DECR` command is crucial.
If it is greater than zero, it indicates that the product is still available, and the purchase can proceed.
Conversely, if the return value is zero or less, it signifies that the product is sold out, and further purchases should be denied.
This method is easy to implement and particularly effective for straightforward scenarios where immediate inventory updates are sufficient and more complex processes like order cancellations can be managed later.

### 2. Atomic Operations with Lua Scripts

For more complex scenarios, Lua scripts can be used to implement atomic inventory verification and deduction.
It is notable that Dragonfly allows non-atomic operations in Lua scripts with the `disable-atomicity` script flag, as explained in this [blog post](https://www.dragonflydb.io/blog/leveraging-power-of-lua-scripting).
Thus, we need to make sure that the script flag is not used for our e-commerce inventory deduction scenario.
Let's assume that we store the inventory of an item using the `Hash` data type:

```shell
dragonfly$> HSET item_on_sale:<item_id> inventory 100 purchased 0
```

To verify and deduct inventory, we can use the following Lua script:

```lua
-- atomic_inventory_deduction.lua

local key = KEYS[1];
local num_to_purchase = tonumber(ARGV[1]);

if num_to_purchase <= 0 then
   return nil;
end

local item = redis.call("HMGET", key, "inventory", "purchased");
local inventory = tonumber(item[1]);
local purchased = tonumber(item[2]);

if purchased + num_to_purchase <= inventory then
   redis.call("HINCRBY", key, "purchased", num_to_purchase);
   return num_to_purchase;
end

return nil;
```

In the script above, we first parse the keys and arguments passed to the script.
The script operates on a single key, which is the key of the item on sale.
Similarly, the script expects a single argument, which is the number of units to purchase.
Then, we retrieve the current inventory and the number of units purchased for the item using the `HMGET` command.
If the total number of units purchased plus the number of units to purchase is less than or equal to the inventory, we increment the number of units purchased and return the number of units purchased.
Otherwise, we return `nil` to indicate that the purchase cannot proceed.
The script above can be executed using the `EVAL` command:

```shell
# General syntax of the 'EVAL' command:
#   EVAL script num_of_keys [key [key ...]] [arg [arg ...]]

# Try to purchase 5 units of the item:
dragonfly$> EVAL "<script>" 1 item_on_sale:<item_id> 5
```

Alternatively, we can load the script and use the `EVALSHA` command, which is more efficient as it stores the script in Dragonfly:

```shell
# Load the script into Dragonfly and get the SHA1 digest of the script.
dragonfly$> SCRIPT LOAD "<script>"

# Try to purchase 5 units of the item using the SHA1 digest of the script:
dragonfly$> EVALSHA "<script_sha>" 1 item_on_sale:<item_id> 5
```

In comparison to the `DECR` command, the Lua script option allows for more complex inventory verification and deduction logic and covers more edge cases.
For instance, in the script above, we have a sanity check to ensure that the number of units to purchase is greater than zero.
In the meantime, we allow purchases of more than one unit of the item, and it handles the edge case where the inventory is not sufficient to fulfill the requested quantity nicely.

### 3. Distributed Locks with Conditional `SET`

Another powerful feature of Dragonfly is its ability to act as distributed locks, playing a critical role in handling surges of traffic.
During high traffic periods, such as flash sales, each incoming request attempts to acquire a lock from Dragonfly.
Only the request that successfully secures a lock gains the exclusive right to proceed with further operations for that particular on-sale item.
These operations might include database operations, payment processes, or any other actions that require direct interaction with the primary database or third-party services within the e-commerce platform.

Requests that fail to acquire a lock are denied further processing and can be redirected to a waiting page or a retry page with a countdown timer, depending on the implementation.
This ensures that **only one request is allowed to proceed at a time per on-sale item**, preventing the primary database from being overwhelmed by excessive simultaneous requests.

A common logic for using distributed locks could be something similar to the following pseudocode:

```javascript
// purchase_item_pseudocode.js

const itemId = getItemId();
const userId = getUserId();
const expiration = getLockExpirationTime();

const lockKey = `item_lock:${itemId}`;
const lockVal = userId;

const lockAcquired = acquireLock(lockKey, lockVal, expiration);

if (!lockAcquired) {
    sendResponse("Another user got this item, please try again later.");
}

try {
    purchaseItem(itemId, userId);
} catch (purchaseError) {
    throw purchaseError;
} finally {
    releaseLock(lockKey, lockVal);
}

sendResponse("You have successfully purchased the item!");
```

It is notable that both `acquireLock` and `releaseLock` take the item ID and user ID into account.
We want to ensure that an acquired lock cannot be accidentally released by another user under high concurrency situations, the `releaseLock` implementation should conform to this requirement.
Also, the choice of the lock expiration time is important.
It should be long enough to allow the user to complete the purchase process, but not too long to prevent other users from acquiring the lock if the service process dies unexpectedly without releasing the lock.

The `acquireLock` function can be implemented in Dragonfly using the `SET` command with the `NX` and `EX` options:

```shell
# Using the 'SET' command with the 'NX' option to acquire a lock.
# The 'NX' option ensures that the lock is only acquired if the key does not exist.
# Also, we set the expiration time for the lock to prevent the lock from being held indefinitely.
dragonfly$> SET item_lock:<item_id> <user_id> NX EX <expiration>
```

On the other hand, the `releaseLock` function needs to be implemented in a Lua script to ensure that the lock is only released if the user ID matches the one that acquired the lock.
This can be achieved in Dragonfly using the `EVAL` command with the following Lua script:

```lua
-- release_lock.lua

local key = KEYS[1];
local val = ARGV[1];

local lock_val = redis.call("GET", key);

if lock_val == val then
   redis.call("DEL", key);
end

return nil;
```

### 4. Distributed Locks with RedLock

Using conditional `SET` commands and Lua scripts for distributed locking in Dragonfly is a straightforward yet effective way to manage highly concurrent operations.
However, in environments where an even higher level of reliability and fault tolerance is required, particularly across distributed systems, the **RedLock** distributed lock algorithm adds additional safety.

RedLock is designed to extend the locking mechanism across multiple primary instances of Redis.
Since Dragonfly is highly compatible with Redis, RedLock can be used with Dragonfly instances as well.
RedLock ensures that a lock is acquired and released correctly and consistently across all these instances, enhancing the reliability and integrity of the distributed locking process.

When using RedLock, the `acquireLock` and `releaseLock` functions are normally provided by the client library already.
This typically involves attempting to acquire the lock on multiple instances simultaneously and ensuring that a majority of the instances grant the lock before proceeding.
Similarly, the release process involves trying to release the lock across all instances to maintain consistency.

For more information on RedLock, read the documentation [here](https://redis.io/docs/manual/patterns/distributed-locks/).

---

## Conclusion

In this blog, we explored how Dragonfly can be used in various ways to elevate your e-commerce platform's performance and user experience.
We discussed how Dragonfly can be used for caching, personalization, and high-traffic flash sales.

Overall, Dragonfly is a versatile and powerful tool for building and maintaining an e-commerce platform, proficient in handling various aspects from everyday user interactions to the most demanding sales events.
Although not directly shown in this blog, Dragonfly's performance is phenomenal, as discussed in detail in our previous [blog posts](https://www.dragonflydb.io/blog).
We encourage you to [try Dragonfly out for yourself](https://www.dragonflydb.io/docs/getting-started) and experience its capabilities firsthand.
Also, consider subscribing to our newsletter below to stay in the loop with the latest Dragonfly news and updates!