The Art (and Pain) of Generating Invoice Numbers That Scale

Ahmad W KhanAhmad W Khan
6 min read

There’s something strangely humbling about a problem that looks trivial on the surface but slowly morphs into an existential crisis at 3 AM while you’re drowning in your 3rd cup of tea.

Invoice number generation? How hard could it be?

I mean, it’s just a bunch of digits, maybe a date prefix, and some unique identifier at the end—right? That’s what I thought when I first encountered this problem years ago as a PHP dev hacking away at an e-commerce project. I needed to generate structured invoice numbers that made sense to humans while ensuring uniqueness at scale.

Sounds simple.

I spent nights crafting an algorithm that was both sequential (because clients love their invoices neatly ordered) and scalable (because databases love their indexes to behave). It worked. The code wasn’t quite elegant to look at, it was a long method but it worked perfectly and I was quite proud of my work back then.

Fast forward to my indie hacking days. The invoice numbering problem crept up again, like an old bug I thought I had long exorcised.

But this time, I had one golden rule: Move fast. Ship the MVP. Worry about elegance later.

The Quick-and-Dirty MVP Approach

Since I was working in Django this time, I whipped up this beauty in record time:

import uuid

class Invoice(models.Model):
    invoice_number = models.CharField(max_length=20, unique=True, editable=False)
    created_at = models.DateTimeField(auto_now_add=True)

    def save(self, *args, **kwargs):
        if not self.invoice_number:
            unique_suffix = uuid.uuid4().hex[:4].upper()
            year = self.created_at.year if self.created_at else "0000"
            self.invoice_number = f"RX-{year}-{unique_suffix}"

        super().save(*args, **kwargs)

It was simple, it worked, and it followed a recognizable pattern (RX-2024-ABCD).

I patted myself on the back and moved on.

Until...

I started seeding the database with thousands of invoices. And that’s when things started breaking.

Duplicate invoice numbers.

At first, I thought: No way. UUIDs are supposed to be unique.

But here’s the catch—randomness ≠ uniqueness when you only take a small slice of a UUID. A 4-character hexadecimal suffix meant just 65,536 possible values (16^4). If I were generating hundreds of invoices per day, I was bound to hit a collision sooner or later.

So at 3 AM, after losing count of how many cups of tea I had consumed, I made a quick fix.


The “Hacky Band-Aid” Solution

from django.db.utils import IntegrityError
from django.utils.timezone import now
import uuid

def save(self, *args, **kwargs):
    if not self.invoice_number:
        for _ in range(10):
            unique_suffix = uuid.uuid4().hex[:4].upper()
            year = self.created_at.year if self.created_at else now().year  
            self.invoice_number = f"RX-{year}-{unique_suffix}"

            try:
                super().save(*args, **kwargs)
                return
            except IntegrityError:
                continue

        raise IntegrityError("Could not generate a unique invoice number after 10 attempts.")

It was an ugly solution, but it got the seeder working. It simply kept retrying until it found a non-duplicate invoice number.

But deep down, I knew this wasn’t the right way to do it. It was duct tape—functional but not elegant.

So I took a step back and thought:

What if I were building this for an actual production system?

Because let’s be real—if this were for a client who had strict format requirements for invoices, I couldn’t get away with just slapping UUIDs onto them.

So here’s what I would’ve done instead.

Production-Grade Solutions for Invoice Numbers

If you want a robust, scalable, production-ready solution, there are a few battle-tested approaches.

1. The Good Old Auto-Incrementing Counter (Database-Backed Sequences)

Most traditional systems rely on sequential invoice numbers because:
They’re easy to track.
Clients expect neatly ordered invoices.
They don’t require extra logic.

Here’s how you can do it in Django:

class Invoice(models.Model):
    invoice_number = models.CharField(max_length=20, unique=True, editable=False)
    created_at = models.DateTimeField(auto_now_add=True)

    def save(self, *args, **kwargs):
        if not self.invoice_number:
            last_invoice = Invoice.objects.order_by("-id").first()
            next_number = (last_invoice.id + 1) if last_invoice else 1
            year = self.created_at.year if self.created_at else now().year
            self.invoice_number = f"RX-{year}-{next_number:06d}"

        super().save(*args, **kwargs)

Pros:

  • Guarantees sequential numbers.

  • No collisions.

  • Client-friendly invoice tracking.

Cons:

  • Can run into race conditions if multiple invoices are created at the same time.

  • Doesn’t scale well in a distributed system.

The race condition issue can be fixed using a database sequence:

CREATE SEQUENCE invoice_seq START 1000;

Then, fetch the next invoice number in Python:

from django.db import connection

def get_next_invoice_number():
    with connection.cursor() as cursor:
        cursor.execute("SELECT nextval('invoice_seq')")
        return cursor.fetchone()[0]

This approach ensures uniqueness at the database level while keeping numbers sequential.


2. A Hybrid Approach (Hash + Incremental Counter)

Let’s say you want a mix of uniqueness and human-readable formatting.

You can hash some metadata (like the timestamp + an incremental counter) to generate a semi-random but unique invoice number:

import hashlib

class Invoice(models.Model):
    invoice_number = models.CharField(max_length=20, unique=True, editable=False)
    created_at = models.DateTimeField(auto_now_add=True)

    def save(self, *args, **kwargs):
        if not self.invoice_number:
            count = Invoice.objects.count() + 1
            hash_suffix = hashlib.md5(f"{self.created_at}{count}".encode()).hexdigest()[:6].upper()
            self.invoice_number = f"INV-{self.created_at.year}-{count}-{hash_suffix}"

        super().save(*args, **kwargs)

Pros:

  • Reduces risk of collisions.

  • More uniqueness than a raw auto-increment counter.

Cons:

  • Not strictly sequential.

  • A little harder to debug.

What Did I end up doing to get a good night’s sleep?

I had two non-negotiable priorities:

Uniqueness—I needed absolute uniqueness across millions of records.
Scalability—The solution had to work seamlessly at scale without worrying about race conditions, sequences, or central control.

Everything else—sequence, readability, even aesthetics—did not matter. And since it was my own thing I was working on, I could always choose a production-ready algorithm before launch.

In fact, the non-sequential nature of UUIDs was an advantage.
The product I was building was handling sensitive transactions, and having non-sequential numbers meant:

  • No one could infer how many invoices had been generated.

  • No one could estimate the number of transactions happening on the platform.

  • It added a layer of data obfuscation—which is great if you don’t want competitors or even malicious users trying to estimate internal metrics by backtracking invoice numbers.

Also, I planned to implement QR code validation for invoices, where users could scan a QR code and hit a public verification API. The API would use the UUID as the primary identifier for validation, making it an easy and reliable lookup key.

So for my use case, UUIDs were perfect.

If this were an e-commerce platform where customers and accountants expected structured, human-readable invoice numbers, UUIDs wouldn’t have been ideal. But in my case, the randomness and uniqueness were exactly what I needed.

That’s the thing with designing systems—it’s all about trade-offs.

Final Thoughts

If you’re generating unique numbers at scale, think about:
Pattern Requirements: Do the invoice numbers need to be human-friendly?
Scalability: Can the approach handle millions of records?
Concurrency: Will multiple invoices be created at the same time?
MVP vs. Production: Do you really need a fancy numbering scheme right now?

If you want to discuss problems like these or anything backend-related over a cup of tea, then feel free to give me a visit at AhmadWKhan.com

Thanks for reading. Happy Hacking!

0
Subscribe to my newsletter

Read articles from Ahmad W Khan directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ahmad W Khan
Ahmad W Khan