Reference codes are an essential part of transactional applications. We encounter it as users of information systems ourselves, in receipts, emails, SMS notifications; everywhere. But because these codes are something that users may have to work with (i.e. write, type, or read out aloud to another person rather than just read silently), their creation must be designed with human usability in mind.
If it were just computers talking to other computers, solutions like UUIDs are a very good fit. Indeed, distributed systems often use UUIDs as the reference code (or identifier) in communication interfaces and protocols. Understandably, developers in a hurry are tempted to adapt UUIDs for reference codes intended for human use. There are systems where the reference code is a Version 4 (randomly-generated) UUID truncated to eight or so characters, and while this works in many cases, this design is not recommended. 1
The way I’ve implemented reference code generation for a production system looks something like this Python function: 2
import random
def generate_reference_code():
choices = "ACDEFGHJKMNPQRTUVWXYZ234679"
return "".join([random.choice(choices) for _ in range(8)])
This produces a string like 9QD69V6J
, which possesses the usability properties of readability and length.
For readability, I’ve limited the possible letters to only upper-case ASCII letters. This removes the need for users to differentiate between small and capital letters. Characters that tend to be confused with each other are also removed: capital B
and the number 8
; the letter S
and the number 5
; the letter O
and the number 0
; and the letter I
and the number 1
. The letter L
, in upper case, is distinct from I
and 1
, but it’s still excluded to be extra-careful about ambiguity, as people can be mindful of the fact that lower-case l
can be confused with i
/1
, and they might carry this expectation (and hesitation) every time they’re faced with reference codes, regardless of source.
(Initially, the character-pairs already widely known to be challenging to tell apart—O
/0
, I
/1
/l
/i
—were the only ones excluded, but I removed more after our users on the field reported issues with those particular characters, especially S
/5
. The use of monospace typefaces isn’t a sufficient solution to this issue, since these codes are also being written down by hand.)
To further improve readability, when printing these codes, developers might opt to insert separators to split the code into clusters, for example with a hyphen as in 9QD7-9V6J
. This provides a hint for users as to how they might “chunk” or cluster the characters when, say, reading it out loud. Devs just need to ensure that the presentation is as consistent as possible in all interfaces, both when displaying the value and when asking for user input (e.g. in forms, such that the input field is also broken up with the separators). This entails additional implementation complexity, however.
The length of eight characters is designed to fit into users’ working memory, allowing them to more easily replay (i.e. immediately recall) the entire code in one go. 3 Of course, in this respect a shorter code is even more usable, but we’re balancing the design against the technical requirement of uniqueness and capacity. With 27 possible values for each of the 8 characters, there are 27^8
or 282.4 billion possible combinations, which should be sufficient for many systems and use cases.
On the use of the Python random
library, note that there is an option to use secrets
instead. (This is in fact what I initially did.) The idea is to imbue the resulting reference code with cryptographically secure properties, but this is unnecessary because reference codes are usually not meant to be secret, and even if there is some advantage to keeping them somewhat confidential, the use of secrets
might only create a false sense of security by obscurity. Therefore random
should be sufficient.
Lastly, there could be a concern about meaningful words, embarrassing ones, showing up in the resulting codes. For example, 3XFUCKYU
. One solution is to remove all remaining vowels from the choices
(A
, E
, and U
, reducing the choices by just 3 characters), though this still could allow results like SH1TFVCK
—at which point perhaps we should just laugh at the misfortune.
[1] UUIDs are intended to be used in their entirety, and truncated UUID strings are not guaranteed to have the same properties as complete ones. One of the potential issues with this is the increased chance of collisions, which in high-volume systems can cause an increase in errors, or at least slow them down if error-handling routines are actually implemented.
[2] As mentioned in “When natural keys act unnaturally”.
[3] There is an observation from psychological studies, sometimes called Miller’s law, that humans are able to recall approximately “seven, plus or minus two” units of information immediately after being presented with it.