Günther Debrauwer - January 17th, 2021

Generating unique, random-looking voucher codes

When you have an application with paid features, you might want to distribute voucher codes to give users (free or cheaper) access to your application. Generating a code in a specific format might be easy. Ensuring that every voucher code is unique and seems random, that's not so easy. That is especially the case if you need to generate thousands (or millions) of voucher codes. In this post, I will explain how you can solve this problem.

The most straightforward approach

Let’s say you want to generate voucher codes that are 6 characters long. The most straightforward approach is shuffling a list of allowed characters and just selecting the first 6 characters. You now have a voucher code, but you can not immediately save it. You will need to check if the voucher code already exists because you used randomization to generate the voucher code. If it does already exists, a new one needs to be generated.

$characters = '123456ABCDEF';

// If the generated voucher code already exists, then a new one should be generated.
while (! isset($code) || Voucher::firstWhere('code', $code) !== null) {
	$code = substr(str_shuffle($characters), 0, 6);
}

Voucher::create(['code' => $code]);

This approach will work just fine if you only need a couple of voucher codes, but it will not be very performant if you need thousands of voucher codes. It will take ages to generate thousands of codes because every time you generate a voucher code you have to check if the voucher code already exists. The randomization causes this issue. You need a unique input for each voucher code.

Encoding a unique number

To remove the randomization factor, you need a unique input for each voucher code. You probably save your voucher codes in a database, so you already have the most basic unique input possible: an incrementing integer.

How do you now convert this number into a voucher code of 6 characters? That is possible using encoding. Similar to base64-encoding, you can create your own encoding system. In this case, the character list consists of 12 characters. That means the number will be "base12-encoded":

$characters = '123456ABCDEF';

$voucherCode = '';

// Encoding the integer.
for ($i = 0; $i < 6; $i++) {
	$digit = $number % strlen($characters);

  	$voucherCode = $characters[$digit] . $voucherCode;

	$number = $number / strlen($characters);
}

return $voucherCode;

Any number can be encoded into a string of 6 characters long. The only caveat is that the encoded result will not be unique for every number in the universe. To determine the largest number that can be encoded into a unique string, you must calculate the number of unique character combinations that can be created. In this example, the encoding system uses the following setup:

  • The character list consists of 12 characters
  • The encoded result must be 6 characters long

So the amount of unique character combinations in this example equals (12*12*12*12*12*12)-1 = 2985983. If you would try to encode any number that is larger than 2985983, you would get the same result as when you encode the number 0. This means the character list and the voucher code length must be chosen based on the requirements of your project.

If you now encode a couple of numbers with this approach, you will get the following results:

Examples:
1 => 111112
2 => 111113
3 => 111114
4 => 111115
5 => 111116
6 => 11111A
7 => 11111B
…

Each of these generated voucher codes will be unique, but they have one big flaw: they are predictable. That’s not a flaw you want if, for example, a voucher code grants a user free access to a paid feature in your app. How do you prevent this? Well, it’s time for some mathematics.

Obfuscating a number using mathematics

To make the voucher codes seem random, you can use a mathematics concept called "modular multiplicative inverse": it allows you to obfuscate every number before encoding it.
For this obfuscation, you need two prime numbers:

  • The first prime number is what we will call the "max prime number". This prime number minus one is the largest number you can obfuscate, so it also limits how many voucher codes you will be able to generate. Choose this one wisely based on the needs of your project.
  • The second prime number is what we will call the "obfuscating prime number". It is responsible for the obfuscation and must be larger than the first prime number.

To keep it simple in this example, 3469 will be the max prime number, and 8311 will be the obfuscating prime number. In your project, you probably want larger prime numbers. Those can be easily generated using an online tool (e.g. https://bigprimes.org).

Let’s have a look at the concept itself. The "modular multiplicative inverse" concept means the following: by multiplying a number with the obfuscating prime number (8311) and then determining the remainder of the division of that multiplication by the max prime number (3469), you will get a unique result. Not any other input number, smaller than 3469, will give you the same result.

$obfuscatedNumber = ($inputNumber * $obfuscatingPrimeNumber) % $maxPrimeNumber

// Examples:
1373 = (1 * 8311) % 3469;
2746 = (2 * 8311) % 3469;
 650 = (3 * 8311) % 3469;
2023 = (4 * 8311) % 3469;
3396 = (5 * 8311) % 3469;
1300 = (6 * 8311) % 3469;
2673 = (7 * 8311) % 3469;

If you now encode every obfuscated number, you will have unique, random-looking voucher codes.

1. Unique input number
2. Obfuscate number using "modular multiplicative inverse"
3. Encode the obfuscated number

Examples:
1 => 1373 => 111DA6
2 => 2746 => 112B1E
3 =>  650 => 1115A3
4 => 2023 => 11231B
5 => 3396 => 112FB1
6 => 1300 => 111D15
7 => 2673 => 112AAD

If you use larger prime numbers and your character list is longer and shuffled, the obfuscation will be even better. The voucher codes will look even more random and unpredictable.

- Use '7230323' and '9006077' as prime numbers instead of '3469' and '8311'
- Use 'LQJCKZMWDPTSXRGANYVBHF' as character list instead of '123456ABCDEF'

Examples:
1 => 1775754 => LWXNHJ
2 => 3551508 => LACSVK
3 => 5327262 => QLNMNM
4 => 7103016 => QDWQGD
5 => 1648447 => LWLYBP
6 => 3424201 => LGRXYS
7 => 5199955 => QLKWAR

A PHP package to make it simple

If you want to generate voucher codes in your project using this technique, you can use the PHP package we created at Next Apps.

use NextApps\UniqueCodes\UniqueCodes;

// Generate 100 unique codes for numbers 1 to 100
$codes = (new UniqueCodes())
    ->setObfuscatingPrime(9006077)
    ->setMaxPrime(7230323)
    ->setCharacters('LQJCKZMWDPTSXRGANYVBHF')
    ->setLength(6)
    ->generate(1, 100);

// Result: LWXNHJ (1), LACSVK (2), QLNMNM (3), ... , LYMJHL (100), QJVBVJ (101), LQXGQC (102), ... , LJQ5DJ (7230320), LC17CS (7230321), LZ8J8H (7230322)

With a very developer-friendly API, you can very easily start generating your unique voucher codes. The package also ensures that the setup you provide is valid (e.g. prime numbers, character list, code length). You can check it out on Github.