DEV Community

Cover image for Create a Scalable Naming System
lrth06
lrth06

Posted on • Edited on

Create a Scalable Naming System

How to tackle the problem of naming in high-volume environments

Originally posted here

Introduction

If you've ever created a project with Google Cloud or Docker you might have noticed every new project gets a unique, randomly-generated name. This is an effective way to avoid naming collisions, with the added benefit of being able to easily track your projects. Let's look at how to create a scalable naming system like these, and how to put it to use in a few different ways.

Getting started

The first thing we need to do is initialize our Node.js application. To do this quickly, lets run npm init -y in our project directory. This will create a generic package.json file for us, and we can edit it to our liking later.

Setting up the file structure

When creating an application intended to scale, modularity is key. Organization is also important as other developers could eventually be working on your projects. Our file structure will look something like this:


├── package.json
├── src/
 |- index.js
 |- lists/
 |-  |- nouns.js
 |-  |- adverbs.js

Enter fullscreen mode Exit fullscreen mode

Our word lists

I've created some pretty lengthy lists of adverbs and nouns for you to use. You can find them here, or create your own lists if you'd like.

Make use of the lists

We need to update our application to import our lists, update the following code:
src/index.js

const adverbs = require("./lists/adverbs.js");
const nouns = require("./lists/nouns.js");
Enter fullscreen mode Exit fullscreen mode

Now that they're imported, we can use them in our application. For now, lets just see how many words we have in each list. Add the following code:
src/index.js

console.log("Adverbs: ", adverbs.length);
console.log("Nouns: ", nouns.length);
Enter fullscreen mode Exit fullscreen mode

If you used my lists, you should see something like this:

Adverbs:  710
Nouns:  1097
Enter fullscreen mode Exit fullscreen mode

Randomize all the things

For the purposes of this application, random is a good thing, each element of our name should be random and unique. To return a random element from our list, we can use the following function expression:
src/index.js

const getRandomWord = (array) => {
  const randomIndex = Math.floor(Math.random() * array.length);
  return array[randomIndex];
};
Enter fullscreen mode Exit fullscreen mode

We also need to create a random number, for now we'll stick with a 4 digit number. To do this, we can use the following function expression:
src/index.js

const getRandomNumber = () => {
  let randomNumber = Math.floor(Math.random() * (9999 - 1000) + 1000);
  return randomNumber;
};
Enter fullscreen mode Exit fullscreen mode

Putting it all together

We need to create a function that will get a random word from each of our lists, and add a number to the end. First, we'll add the following:
src/index.js

const adverb = getRandomWord(adverbs);
const noun = getRandomWord(nouns);

const combineWords = (adverb, noun) => {
  let result = `${adverb}-${noun}-${getRandomNumber()}`;
  return result;
};

module.exports = console.log(combineWords(adverb, noun));
Enter fullscreen mode Exit fullscreen mode

If we've done everything correctly, we should be able to call our function and get a random name. To test this, we can run our application in our terminal:

node src/index.js
Enter fullscreen mode Exit fullscreen mode

and we should see something like this printed out:

chronologically-broken-4521
Enter fullscreen mode Exit fullscreen mode

Voila! We've got a random name! Now we just need to make sure our application prevents collisions, if not, we could end up with the same name multiple times.

Keeping track of it

This is the most basic way to use our application, but we're not tracking what we've already used, so we'll need to add some logic to make sure we don't get the same name twice. For this, we'll need a datastore that is as scalable as our application. In this case, we'll use Redis, a very popular and scalable database, and ioRedis, a Node.js wrapper for Redis.

We'll need to initialize the redis client, to do so add the following code:
src/index.js

const redis = require("ioredis");
const client = new redis(process.env.REDIS_URL);
Enter fullscreen mode Exit fullscreen mode

And then add a function which will check if a redis key with the value of our name exists. If it does, we'll need to generate a new name. Once a unique name is generated, we need to return the name. To do this, we'll need to add the following code:
src/index.js

async function getRandomName() {
  const name = combineWords(adverb, noun);
  const exists = await redis.exists(name);
  if (exists !== 0) {
    console.log("Cache hit, generating new name");
    await redis.incr("duplicates");
    return getRandomName();
  }
  await redis.set(name, name);
  return name;
}
Enter fullscreen mode Exit fullscreen mode

if you've been following to this point, your src/index.js file should look something like this:

const Redis = require("ioredis");
const redisUri = process.env.REDIS_URI;
const redis = new Redis(redisUri);
const adverbs = require("./lists/adverbs");
const nouns = require("./lists/nouns");
const getRandomWord = (array) => {
  const randomIndex = Math.floor(Math.random() * array.length);
  return array[randomIndex];
};

const adverb = getRandomWord(adverbs);
const noun = getRandomWord(nouns);
const randomNumber = () => {
  let randomNumber = Math.floor(Math.random() * (9999 - 1000) + 1000);
  return randomNumber;
};

const combineWords = (adverb, noun) => {
  let result = `${adverb}-${noun}-${randomNumber()}`;
  return result;
};

async function getRandomName() {
  const name = combineWords(adverb, noun);
  const exists = await redis.exists(name);
  if (exists !== 0) {
    console.log("Cache hit, generating new name");
    await redis.incr("duplicates");
    return getRandomName();
  }
  await redis.set(name, name);
  return name;
}
Enter fullscreen mode Exit fullscreen mode

Deploying as a serverless function

In this case, we're going to use the gcloud command line tool to deploy our application as a serverless function to Google Cloud Functions. To accomplish this, we'll need to wrap our application in an asynchronous function expression. Update the following code:
src/index.js

const Redis = require("ioredis");
const redisUri = process.env.REDIS_URI;

exports.generateName = async (req, res) => {
  const redis = new Redis(redisUri);
  const adverbs = require("./lists/adverbs");
  const nouns = require("./lists/nouns");
  const getRandomWord = (array) => {
    const randomIndex = Math.floor(Math.random() * array.length);
    return array[randomIndex];
  };

  const adverb = getRandomWord(adverbs);
  const noun = getRandomWord(nouns);
  const randomNumber = () => {
    let randomNumber = Math.floor(Math.random() * (9999 - 1000) + 1000);
    return randomNumber;
  };

  const combineWords = (adverb, noun) => {
    let result = `${adverb}-${noun}-${randomNumber()}`;
    return result;
  };

  async function getRandomName() {
    const name = combineWords(adverb, noun);
    const exists = await redis.exists(name);
    if (exists !== 0) {
      console.log("Cache hit, generating new name");
      await redis.incr("duplicates");
      return getRandomName();
    }
    await redis.set(name, name);
    return name;
  }

  const name = await getRandomName();
  res.send(name);
};
Enter fullscreen mode Exit fullscreen mode

This change allows our function to be used as a serverless function which can be utilized by a variety of mediums. Run the following command in your terminal to authenticate with gcloud and deploy the function:

NOTE You'll need to have a project created in Google Cloud before you can deploy your function. You'll also need the Cloud Build API enabled in your project.

gcloud auth login
gcloud config set project [YOUR_PROJECT_ID]
gcloud functions deploy generateName --runtime nodejs16 --trigger-http --allow-unauthenticated --set-env-vars REDIS_URI={YOUR_REDIS_URI}
Enter fullscreen mode Exit fullscreen mode

This process will take a few minutes to complete, and once complete, there will be a great deal of information printed out to the console. We can see that our function is deployed and we can see the URL of the function. This URL can be used to call our function from a client, or directly from the browser for testing.

Putting it to use

In this case, we'll use the function to name projects locally utilizing a bash script, knowing we can reuse the same endpoint in later applications. In a new directory, create a file called new-project.sh and add the following code:
new-project.sh

#!/bin/bash
NAME=$(curl -s {YOUR_FUNCTION_URL})
echo "Creating Directory $NAME"

makeDir(){
    mkdir -p "$NAME" && cd "$NAME"
}
makeDir
Enter fullscreen mode Exit fullscreen mode

To test our bash script, we can run the following command in our terminal:

. ./new-project.sh
Enter fullscreen mode Exit fullscreen mode

If all goes well, we should see a new directory created with a random name from our function, and we should be in that directory ready to start working on our project. The last thing we need to do is create an alias for our bash script. To do this, we'll need to add the following code to our .bashrc file:
.bashrc

alias new-project=". /path/to/new-project.sh"
Enter fullscreen mode Exit fullscreen mode

This will enable us to call our bash script from the command line, no matter which directory we're in. To test our new alias, we can run the following command in our terminal:

new-project
Enter fullscreen mode Exit fullscreen mode

Which should now output something like this:

Creating Directory consquentially-silent-8851
  consquentially-silent-8851
Enter fullscreen mode Exit fullscreen mode

Conclusion and next steps

This serverless function can go on to be used in numerous applications with ease, and solves a very real problem with minimal effort. Stay tuned for more on this topic and others in the future!

Top comments (0)