Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

infoflopaydeveloper's avatar

How generate unique invoice number and avoid race condition

Can anyone help me to show an example of how to generate a unique invoice number and avoid a race condition?

Someone on discussion says use atomic lock https://laravel.com/docs/8.x/cache#atomic-locks

But don't know how to use it. Any other idea is also welcome.

0 likes
29 replies
sr57's avatar

Locks are MANDATORY to avoid race condition, ie bug (deadlock, ... ) with 'concurrent apps' but not useful with DB (except if cache is used). DB by conception follow ACID concept (lock built inside)

https://www.lifewire.com/the-acid-model-1019731

In standard all tables in Laravel have a unique number (id) , your can create new ones with the ->unique() when you need.

infoflopaydeveloper's avatar

But I don't want to use the id column. As you know invoice numbers are in a different format than the id column. In my case, the invoice number is a minimum of 4 digits like 0001. Let says I have my own method to generate invoice numbers now how can I avoid race conditions on concurrent requests.

With a unique column, if a concurrent request happens it will throw an exception. Another thing is that I have a multi-tenant app that has a site_id column in this case, it should be unique on each tenant not in general.

sr57's avatar

Everything is written in my answer

2nd (and last till next Monday :-) ) time

I don't want to use the id column.

you can create new ones as you want

if a concurrent request happens it will throw an exception

Why? your db will always generate an unique invoice number

Another thing is that I have a multi-tenant

No, pb, it'll always be unique (by the way you want ... or the package you'll use will choose for you)

infoflopaydeveloper's avatar

I don't want to use any package for this. And how DB will generate a unique invoice number as it is my own custom method to generate invoice numbers with taking care of multi-tenancy. This means the number can be repeated but should be unique per tenant.

jlrdw's avatar

@infoflopaydeveloper You can still use the id.

Get last id

SELECT LAST_INSERT_ID();

https://dev.mysql.com/doc/c-api/8.0/en/getting-unique-id.html

Now edit same record padding the id in invoice field:

  • id = 137

  • make invoice 0000137 // or whatever.

I include a date on invoices if needed.

I do this all the time. Read that mysql link, you have to take into account if using connection pooling.

I also have another technique, post here if above doesn't work.

infoflopaydeveloper's avatar

I cannot use SELECT LAST_INSERT_ID(); because there are other tables as well and this query will pick last inserted id from any table. That will not solve the problem.

ep!sode's avatar

You need to check the table field in the db first inside a mutator. Maybe somewhat similar to this approach:


Class ModelA extends Model {
 pub func setInvoiceNumberAttribute($value) {
  while(ModelA::where('invoice_number', $value)->exists()) {
   $value = yourInvNumGenerator();
  }

  return $this->attribute['invoice_number'] = $value;
 }
}

Remember that your yourInvNumGenerator() should be the same as the one you have you used on your controller when storing:

pub func store
 ModelA::create([
  ...
  'invoice_number' => yourInvNumGenerator()
  ...
 ]);

Snapey's avatar

One option might be to store the next invoice number as a property of the tenant and then increment it using record locking;

Something like

$tenant=1;

$invoice = DB::transaction(function () use ($tenant) {
    $inv = DB::table('tenants')
        ->where('id', $tenant)
        ->lockForUpdate()
        ->first('next_invoice')
		->next_invoice;

    DB::table('tenants')
        ->where('id', $tenant)
        ->update(['next_invoice' => ++$inv]);

    return $inv;
});


So, this code gets the value from the database and uses a lock to stop the row being updated by another process. It then writes back an incremented version of the invoice number and commits the transaction, freeing the row for another thread.

The presentation of this incrementing number can be handled in the view or with an accessor on the model (ie prefixing with characters or leading zeros.

1 like
infoflopaydeveloper's avatar

This looks closer to what I want. But I have few questions.

With this piece of code let say I have avoided race condition in generating invoice number but what about after that code when we try to create a new invoice and got some errors like validations checks or anything. Then the updated invoice number cannot be reversed as Invoice::create does not trigger a transaction.

Also, I have to reuse generate invoice method that is reusable. I have some other entities that are converted into invoices.

Snapey's avatar

if you generate a new number and then don't use it then. you will have a gap in your numbering

This won't be because of a validation error because in that case you just use the same number since it's part of the same flow

The main issue will be users abandoning the process

It sounds like you don't really need to avoid the race condition, you need to work out what the application workflow should be

infoflopaydeveloper's avatar

There is no chance of abandoned because I will not be going to show the new invoice number on the frontend.

Once a user creates a new invoice or convert estimates to an invoice or subscription entity create a new invoice the everything will be processed server side and that's where a new invoice number code will trigger.

I have already built this system in place in another framework and the code quality is not good.

That's why I am planning to recreate the whole system in laravel.

How https://github.com/Bottelet/DaybydayCRM this is creating invoice number.

Or how to use atomic locking to generate a new invoice number.

Snapey's avatar

as per my example then. Why would it fail? you can write to the model and have the invoice number added through a model observer

jjudge's avatar

@infoflopaydeveloper To work around this, leave the fetching of the invoice number until the end, after all the other parts of the invoice have been validated. As has been suggested, an observer could do this.

jlrdw's avatar

One other technique is have a long list of invoices available in a table of just invoice numbers, as you use the next one soft delete it there or have a true false field that it's in use.

This is one of the technique I have used in the past and it worked. This ensures no duplicates and you don't have to worry about transactions locking and all that stuff.

If used just work out the details. In my case I would have the last four digits would be the year.

Also it works better than generating a random, because sometimes you can have collisions when generating a random number.

Snapey's avatar

It doesn't solve the race condition as two threads could still grab the same 'next number'

newbie360's avatar

i don't understand, why can't use id field ?

public function getInvoiceNumberAttribute()
{
    return $this->attributes['shop_code'] . '-' . str_pad($this->attributes['id'], 5, 0, STR_PAD_LEFT);
}

// ShopA-00001
tusharsawant242726's avatar

You could try this solution.

#Invoice Number: INV-20210102-01 (prefix+YYYY+MM+DD)

$order_prefix='INV-';

$get_latest_order=DB::select("SELECT REPLACE(order_display_no, '$order_prefix', '')+1 AS next_number from orders WHERE order_display_no LIKE '$order_prefix%' ORDER BY orders_id DESC LIMIT 1");

if(!empty($get_latest_order[0])){
    
    $order_no=str_pad($get_latest_order[0]->next_number, 2, "0", STR_PAD_LEFT);
}
else{
    $order_no=01;
}

$order_display_no=$order_prefix.$order_no;
jlrdw's avatar

@infoflopaydeveloper

You could have one extra field in table, user_id, and get the max id inserted by that user in that table, really one extra field wouldn't be a big deal. This way you know the id is the correct id to build invoice off of.

Basically add record, then turn around and edit it to add the invoice number.

You are looking for the record with a maxid of the user_id

$forinvoice = DB::table('yourtable')->max('id')
                                          ->where('user_id', $Auth::id);

Something like that.

Remember add first, then edit to build invoice.

I just typed out quickly, adjust as needed, you are after:

The max id of a record where a user_id is the id of the user who is making an invoice.

You will have no collisions, as long as the edit is the very next thing done.

And as stated pad with zero's as needed.

Snapey's avatar

i gave you an example using transactions

Your solutions are using an autoincrementing id or using a transaction.

sr57's avatar

Hi @infoflopaydeveloper

Several people in this thread gave you an good example.

With this thread you should have understood that :

  • race condition can be avoid simply by unique db transaction

  • using db does not mean you avoid race condition

  • you can define UUID AS YOU WANT

If you still does not understand, the best way is probably to code by yourself and ask if it's correct (regard race condition for instance)

infoflopaydeveloper's avatar

Code was already written and facing race condition issue. Of course, it's not in laravel but logic is the same as getting max invoice number and increment to it.

I don't want to use UUID because I have my own format to generate an invoice number.

jlrdw's avatar

I didn't say:

but logic is the same as getting max invoice number and increment to it.

What I was saying, let's say you just entered a record, and one of the fields is user_id.

Now that new record has an ID also, let's pretend it's ID 127.

Let's Pretend the user entering the record has user_id of 5.

now you want to find the max ID of record where the user_id = 5.

The max id is 127. The aggregate function tells you that.

Now you know which record to find and edit in the invoice information.

This way no locks are required, otherwise use your locks.

Edit: so invoice number is 000127. Or however you pad the invoice number.

sr57's avatar

@infoflopaydeveloper

Code was already written and facing race condition issue

So close the subject (and share your code)

That said, communication is always a difficult art.

Of course, it's not in laravel but logic

Don't understand ...

is the same as getting max invoice number and increment to it.

Like we told you from the beginning (auto)increment is the easiest way to make an UID

I don't want to use UUID because I have my own format to generate an invoice number.

UID, I did a small mistake in my previous post.

Hope your invoice number is UID :-)

justinmoh's avatar

@sr57 @snapey I think the question could be asked another way round.

Here's an example, "Invoice Number" format requirement :

 {office_location}-{year}-{month}-{sales_person}-{sequential_id_for_the_month}

How do you ensure a non-duplicate and sequential identity reference string for the purpose?

yeah, at part of the globe where I live in, "Invoice Number" is not pure number, sort of like "your car plate number".

Snapey's avatar

@justinmoh by using an atomic lock whilst you examine the database and decide what the next number should be.

justinmoh's avatar

@Snapey yeah the OP aware of this and trying to request a working implementation steps or pseudo code.

But I solve these issue with single worker queue... so far data seems intact.

Please or to participate in this conversation.