DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Providing Enum Consistency Between Application and Data
  • The Bill You Didn't See Coming
  • Retiring a Tier-0 Legacy Database Without Breaking the Business
  • The Aggregate Reference Problem

Trending

  • DZone's Article Submission Guidelines
  • How to Submit a Post to DZone
  • 7 Technology Waves I’ve Seen in 30 Years of Software — Will AI Be the Next Real Transformation?
  • Implementing Observability in Distributed Systems Using OpenTelemetry
  1. DZone
  2. Data Engineering
  3. Data
  4. Building a Real-Time Ad Server With Dragonfly

Building a Real-Time Ad Server With Dragonfly

Develop a real-time ad server with Bun and ElysiaJS for seamless integration. Explore hands-on examples to grasp these cutting-edge technologies.

By 
Joe Zhou user avatar
Joe Zhou
·
Jan. 26, 24 · Tutorial
Likes (1)
Comment
Save
Tweet
Share
2.5K Views

Join the DZone community and get the full member experience.

Join For Free

Ad-serving systems frequently need to accommodate millions, or even billions, of requests daily, constructing and delivering personalized ads within just a few milliseconds. Beyond handling the substantial daily volume of requests, these platforms must be capable of managing spiky traffic with sudden surges in user activity as well.

In this blog post, we will construct a real-time ad cache server utilizing the cutting-edge technologies of Bun, ElysiaJS, and Dragonfly. Not just for the enjoyment of exploring new tools, but also to leverage their exceptional developer experience and performance capabilities. Let's take a brief look at the technologies we will be using in this post:

  • Bun, an all-in-one JavaScript/TypeScript runtime and toolkit that reached 1.0 a month ago, stands out for speed and efficiency, making it a perfect choice for high-performance applications.
  • ElysiaJS is a TypeScript framework supercharged by Bun with end-to-end type safety and outstanding developer experience.
  • Last but not least, Dragonfly, our high-throughput, multi-threaded in-memory data store, often acts as a robust drop-in replacement for Redis, capable of handling up to 4 million QPS and managing 1TB of workload on a single machine.

The code snippets provided throughout this blog post can be found in the dragonfly-examples repository. We strongly encourage you to clone this repository and follow along with the provided code examples as you read through this blog post. Engaging with the code firsthand is an excellent way to get your hands dirty and gain practical experience with the concepts and technologies we are covering.

Ad Serving Functionalities

In this section, we will delve into a sample architecture, showcasing the practical application of Dragonfly in the context of an ad server. Within an ad-serving platform, two fundamental and commonly utilized functionalities include:

  • Ad Metadata Management
  • Ad User Preference Management

We will explore each of these in detail below.

Ad Metadata Management

Effectively managing a diverse array of advertisements is a critical requirement for an ad-serving platform. This entails maintaining comprehensive details for each advertisement, including elements such as the ad title, description, image URL, click URL, and more. The system must be agile, swiftly incorporating new ads, updating existing entries, and retrieving ads when they are requested for display. The Hash data type that Dragonfly supports is a perfect candidate for this.

dragonfly$> HGETALL ad:metadata:1
1) "id"
2) "1"
3) "title"
4) "Dragonfly - a data store built for modern workloads"
5) "category"
6) "technology"
7) "clickURL"
8) "https://www.dragonflydb.io/"
9) "imageURL"
10) "https://www.dragonflydb.io/blog"


Ad User Preference Management

Ad prioritization tailored to user preferences not only enhances the overall user experience but also significantly boosts ad effectiveness, driving higher engagement and conversion rates. By serving ads that align with user preferences, ad platforms can maximize their value for both advertisers and users. Let's assume we categorize ads into various categories (such as sports, technology, fashion, etc.), and we also track the categories each user is interested in. Both ad categories and user preferences can be efficiently stored using the Set data type that Dragonfly supports, with each ad category having a category-set, and each user having a preference-set. Upon a user's visit, we can retrieve their preferences, search for corresponding ads for each category, and subsequently display ads that align with the user's preferences.

dragonfly$> SMEMBERS ad:category:technology
1) "1"
2) "2"
dragonfly$> SMEMBERS ad:user_preference:1
1) "sports"
2) "technology"

As shown above, the ad category technology has two ads with IDs 1 and 2. The user with ID 1 has two preferences: sports and technology.

Run Dragonfly and Ad Server Application

1. Prerequisites

  • Make sure Docker is installed and running locally, which will be used to run Dragonfly.
  • Make sure Bun is installed locally. We will be using Bun to run our TypeScript application.

2. Start Dragonfly

First of all, let's make sure that we have Dragonfly running locally so that it is easier to run the sample application. There are a few ways to get started with Dragonfly. For this blog post, we will be using Docker to run Dragonfly with just one command:

Dockerfile
 
docker run -p 6379:6379 --ulimit memlock=-1 docker.dragonflydb.io/dragonflydb/dragonfly



Upon successful execution, you should see Dragonfly outputting logs similar to the following:

I20231024 15:43:44.752050     1 init.cc:70] dragonfly running in opt mode.
I20231024 15:43:44.752230     1 dfly_main.cc:798] Starting dragonfly df-v1.11.0-c6f8f3882a276f6016042016c94401242d9c5365
W20231024 15:43:44.752424     1 dfly_main.cc:837] SWAP is enabled. Consider disabling it when running Dragonfly.
I20231024 15:43:44.752456     1 dfly_main.cc:842] maxmemory has not been specified. Deciding myself....
I20231024 15:43:44.752461     1 dfly_main.cc:851] Found 6.58GiB available memory. Setting maxmemory to 5.26GiB
I20231024 15:43:44.758332     9 uring_proactor.cc:157] IORing with 1024 entries, allocated 102720 bytes, cq_entries is 2048
I20231024 15:43:44.782545     1 proactor_pool.cc:147] Running 5 io threads
I20231024 15:43:45.196280     1 snapshot_storage.cc:112] Load snapshot: Searching for snapshot in directory: "/data"
W20231024 15:43:45.196475     1 server_family.cc:466] Load snapshot: No snapshot found
I20231024 15:43:45.207091    11 listener_interface.cc:83] sock[13] AcceptServer - listening on port 6379

3. Clone the Example Repository

As previously mentioned, all the code examples featured in this post are accessible in the dragonfly-examples repository. Clone the repository to your local machine using the command provided below. Once the repository is cloned, proceed by navigating to the ad-server-cache-bun subdirectory, where you'll find all the code snippets used in this blog post.

Shell
 
git clone [email protected]:dragonflydb/dragonfly-examples.git
cd dragonfly-examples/ad-server-cache-bun


4. Install Dependencies and Run the Application

Within the dragonfly-examples/ad-server-cache-bun directory, use Bun to install the dependencies for the real-time ad server application. As shown below, Bun will install the dependencies listed in the package.json file, including ElysiaJS and ioredis.

bun install
# bun install v1.0.6 (969da088)
#  + [email protected]
#  + @sinclair/[email protected]
#  + [email protected]
#  + [email protected]
# 
#  21 packages installed [38.00ms]


Then, use Bun to run the application:

bun dev
# $ bun run --watch src/index.ts
# Ad server API is running at localhost:3888


5. Interact With the Ad Server API

Now that the ad server API is running, we can interact with it using curl or a similar tool. For instance, we can create ads with the following requests:

Shell
 
curl --request POST \
  --url http://localhost:3888/ads \
  --header 'Content-Type: application/json' \
  --data '{
    "id": "1",
    "title": "Dragonfly: An In-Memory Data Store Built for Modern Workloads",
    "category": "technology",
    "clickURL": "https://www.dragonflydb.io",
    "imageURL": "https://www.dragonflydb.io/blog"
}'

curl --request POST \
  --url http://localhost:3888/ads \
  --header 'Content-Type: application/json' \
  --data '{
    "id": "2",
    "title": "Dragonfly Cloud: Fully Managed Dragonfly Service",
    "category": "technology",
    "clickURL": "https://www.dragonflydb.io/cloud",
    "imageURL": "https://www.dragonflydb.io/blog"
}'


Similarly, we can create or update user preferences with the following request:

Shell
 
curl --request POST \
  --url http://localhost:3888/ads/user_preferences \
  --header 'Content-Type: application/json' \
  --data '{
    "userId": "1",
    "categories": [
        "sports",
        "technology"
    ]
}'


Finally, we can retrieve ads for a specific user with the following request. Since the user with ID 1 has two preferences, sports and technology, we expect to see both technology ads created above:

Shell
 
curl --request GET \
  --url http://localhost:3888/ads/user_preferences/1


Shell
 
[
  {
    "id": "1",
    "title": "Dragonfly: An In-Memory Data Store Built for Modern Workloads",
    "category": "technology",
    "clickURL": "https://www.dragonflydb.io",
    "imageURL": "https://www.dragonflydb.io/blog"
  },
  {
    "id": "2",
    "title": "Dragonfly Cloud: Fully Managed Dragonfly Service",
    "category": "technology",
    "clickURL": "https://www.dragonflydb.io/cloud",
    "imageURL": "https://www.dragonflydb.io/blog"
  }
]


Please note that while the example ad server API provided does not adhere strictly to the RESTful style, it serves sufficiently for the purposes of our demonstration.

Implementation Details

Now that we have the ad server API up and running let's take a look at some key places in the codebase.

1. Client Initialization

Dragonfly is fully compatible with the Redis RESP protocol, which is supported by many client libraries and SDKs. In this example, we use the ioredis package to interact with Dragonfly. TypeScript allows import-aliasing. Although this is not required, it can be helpful to make the code clearer as we are emphasizing the connection to Dragonfly:

JavaScript
 
import {Redis as Dragonfly} from 'ioredis';

const client = new Dragonfly();


Also, it is notable that we are not passing any connection parameters to the client constructor. This is because we are running Dragonfly locally using Docker, as explained above, and the default connection address localhost:6379 is sufficient.

2. Key Name Management

Just like Redis, Dragonfly is a key-value in-memory data store. Data is stored in Dragonfly as key-value pairs, where the key is a String, and the value can be one of the supported data types (such as String, Hash, Set, Sorted-Set, etc.). Key-value data stores are often categorized as NoSQL databases. Millions or even billions of keys can be stored in Dragonfly, and thus, it is good practice to use a naming convention for keys within your application. A common practice is to use semicolon-separated key segments, where each segment represents a level of hierarchy:

TypeScript
 
export class AdMetadataStore {
  private client: Dragonfly;
  static readonly AD_PREFIX = "ad";
  static readonly AD_METADATA_PREFIX = `${AdMetadataStore.AD_PREFIX}:metadata`;
  static readonly AD_CATEGORY_PREFIX = `${AdMetadataStore.AD_PREFIX}:category`;
  static readonly AD_USER_PREFERENCE_PREFIX = `${AdMetadataStore.AD_PREFIX}:user_preference`;

  // ...

  // Sample: 'ad:metadata:1'
  private metadataKey(metadataId: string): string {
    return `${AdMetadataStore.AD_METADATA_PREFIX}:${metadataId}`;
  }

  // Sample: 'ad:category:technology'
  private categoryKey(metadataCategory: string): string {
    return `${AdMetadataStore.AD_CATEGORY_PREFIX}:${metadataCategory}`;
  }

  // Sample: 'ad:user_preference:1'
  private userPreferenceKey(userId: string): string {
    return `${AdMetadataStore.AD_USER_PREFERENCE_PREFIX}:${userId}`;
  }

  // ...
}


Combining key prefixes and helper methods, we can easily construct keys for different purposes. And whenever a value needs to be accessed (i.e., by userId), we are confident that the key is constructed correctly.

3. Dragonfly Compatibility

As previously mentioned, Dragonfly is fully wire-protocol compatible and supports 220+ Redis commands. The ioredis package we are using in this example provides strongly-typed methods for each of the supported commands. If a command is supported by Dragonfly, then we can use the corresponding ioredis method directly. For instance, in the createAdMetadata method below, we use both the HMSET command and the SADD command. By doing so, a piece of ad metadata information is stored as a hash value, and its ID is added to the corresponding category-set.

JavaScript
 
export class AdMetadataStore {
  // ...

  async createAdMetadata(metadata: AdMetadata): Promise<void> {
    await this.client.hmset(this.metadataKey(metadata.id), metadata);
    await this.client.sadd(this.categoryKey(metadata.category), metadata.id);
  }

  // ...
}


4. End-To-End Type Safety

The rest of the code example should be straightforward to follow, as explained in the Ad Serving Functionalities section above. It is worth mentioning that Bun, ElysiaJS, and ioredis provide a great developer experience along with Dragonfly, and we are able to leverage the power of these tools to ensure end-to-end type safety throughout the codebase. For instance, each command (like HMSET or SADD) is already strongly-typed, as shown above.

Another example can be illustrated by the handler for the POST /ads endpoint below. We are passing context.body, which is originally of type unknown, to the createAdMetadata method. Since we also specified an input schema hook { body: AdMetadata }, ElysiaJS will magically and automatically validate the input and infer the type of context.body as AdMetadata.

TypeScript
 
const app = new Elysia()
  .decorate("adMetadataCache", new AdMetadataStore(client))
  .post(
    "/ads",
    async (context) => {
      await context.adMetadataCache.createAdMetadata(context.body);
      context.set.status = 201;
      return;
    },
    { body: AdMetadata } // Input Schema Hook
  )
  .listen(3888);


Conclusion

In this blog post, we guided you through the process of building a robust ad-serving application, demonstrating the seamless integration and high compatibility of Dragonfly within our tech stack.

Data Types Database Cache (computing)

Published at DZone with permission of Joe Zhou. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Providing Enum Consistency Between Application and Data
  • The Bill You Didn't See Coming
  • Retiring a Tier-0 Legacy Database Without Breaking the Business
  • The Aggregate Reference Problem

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook