Java/MongoDB: No Locks for Unique Invoice Numbers
Generating unique, sequential invoice numbers is a common requirement in many business applications. A system running Java, JAX-RS, and MongoDB on Tomcat faced a critical issue: under heavy load, simultaneous requests were generating identical invoice numbers. This isn't a theoretical problem; a user reported two invoices with the same folio, a symptom that emerged when two requests arrived just 40 milliseconds apart.
The Symptom: Duplicate Invoice Numbers Under Load
The core of the problem lay in a naive implementation for generating these unique identifiers. The existing code followed a straightforward, but flawed, pattern:
// Antes: lee el último número, le suma 1, guarda
Documento ultimo = dao.findLastByTipo("FACTURA");
int nuevoNumero = ultimo.getNumero() + 1;
nuevoDocumento.setNumero(nuevoNumero);
dao.save(nuevoDocumento);
This sequence, when executed by two concurrent threads, creates a race condition. Thread A reads the last invoice number (e.g., 100). Before Thread A can save its new invoice with number 101, Thread B also reads the last invoice number (still 100). Both threads then proceed to generate and save an invoice with number 101, leading to the critical duplication. Traditional solutions often involve database locks or distributed locks, which can introduce performance bottlenecks and complexity, especially in a distributed environment.
A Lock-Free Approach Using MongoDB's Atomic Operations
The solution implemented bypasses the need for explicit locking by leveraging MongoDB's atomic operations. Instead of a read-modify-write cycle, the system now uses MongoDB's findAndModify operation. This powerful feature allows an update to be performed and the document to be returned in a single, atomic step, preventing other operations from interfering in between.
The new process looks like this:
// Nuevo: usa findAndModify para obtener y actualizar atómicamente
Query query = new Query();
query.addCriteria(Criteria.where("tipo").is("FACTURA");
Update update = new Update();
update.inc("numero", 1);
FindAndModifyOptions options = new FindAndModifyOptions();
options.returnNew(true);
Documento ultimo = mongoTemplate.findAndModify(query, update, Documento.class);
if (ultimo == null) {
// Si no existe, crear el primero
ultimo = new Documento("FACTURA", 1);
mongoTemplate.save(ultimo);
}
// Ahora `ultimo.getNumero()` tiene el número correcto y único
nuevoDocumento.setNumero(ultimo.getNumero());
dao.save(nuevoDocumento);
The key here is findAndModify. This operation atomically finds a document matching the query, applies the update (incrementing the `numero` field by 1), and returns the document *after* the update. If no document exists, it's created with an initial number of 1. This entire sequence is guaranteed by MongoDB to be atomic. Even if two requests hit the database simultaneously, only one will successfully execute the findAndModify and retrieve the incremented number. The other request will either wait or, if configured differently, might get a different result, but crucially, it won't get the same number as the first. The subsequent save operation then uses this guaranteed unique number.
Considerations for Implementation
To ensure this works correctly, a few points are critical:
- Unique Index: A unique index on the `tipo` and `numero` fields in MongoDB is essential. While
findAndModifyprevents duplicates in the incrementing step, the index provides a final safety net against any unforeseen issues or race conditions that might arise from other parts of the system or application logic. - Upsert and Initial Value: The logic must handle the case where no previous document exists for a given type (e.g., the very first invoice). The
findAndModifyoperation, when combined with an upsert (though not explicitly shown in the simplified Java snippet, it's implied by handling the `null` return), can create the initial record. If the document isn't found, the code creates the first document with number 1 and saves it. - Performance: While this approach avoids explicit application-level locks,
findAndModifyitself involves an atomic operation on the database. For extremely high throughput systems, monitoring MongoDB's performance and ensuring appropriate indexing is crucial. However, it's generally far more performant and simpler to manage than distributed locking mechanisms.
Conclusion: Scalable Uniqueness
By replacing a manual read-modify-write cycle with MongoDB's atomic findAndModify operation, the system eliminates the race condition that led to duplicate invoice numbers. This approach provides a robust, scalable, and simpler solution than implementing complex locking strategies, ensuring data integrity even under significant load. It's a testament to how leveraging database-specific atomic primitives can solve common concurrency challenges elegantly.
