Sequence Generation Best Practices
Developer guide for the Enezaframework sequence generation. Part of the Sequence Generation series.
Table of Contents
- Overview
- Choose the Right Base Model
- Declare Models Correctly
- Use Meaningful Prefixes
- Plan Number Lengths
- Test Roll-Over
- Handle Concurrency
- Back Up and Monitor
- Related Guides
Overview
These practices keep sequences predictable and safe to operate. They are about the model side (what to inherit and how to format) and the operations side (lengths, roll-over, concurrency, backups).
Choose the Right Base Model
| Record type | Inherit | Why |
|---|---|---|
| Master data (Customer, Vendor, Product) | EnezaMaster |
Includes SequenceMixin + entity. |
| Documents (Invoice, Order, Journal) | EnezaDocument |
Includes SequenceMixin + entity. |
| Records without a business number | EnezaGeneral |
No sequence support. |
Declare Models Correctly
- Do not inherit
SequenceMixinseparately — it is already inEnezaMaster/EnezaDocument. - Do define both
sequence_field_nameanddocument_name. - Do make the sequence field nullable (
null=True, blank=True). - Do follow the project field conventions (one field per line,
verbose_name, closing parenthesis aligned).
# Correct
class Product(EnezaMaster):
product_code = models.CharField(
max_length=50,
null=True,
blank=True,
verbose_name="Product Code",
)
name = models.CharField(
max_length=200,
verbose_name="Name",
)
sequence_field_name = 'product_code'
document_name = ProjectDocumentTypes.PRODUCT
# Incorrect — risk of MRO problems
class Product(SequenceMixin, EnezaMaster):
...
Use Meaningful Prefixes
Choose prefixes that are short but unambiguous and unique per document type:
| Good | Bad |
|---|---|
CUST, INV, PO, SO, EMP |
C, I, P, S, E |
Keep prefixes consistent across the system so users can recognize record types at a glance.
Plan Number Lengths
| Length | Range | Capacity |
|---|---|---|
| 4 | 0001–9999 |
10,000 |
| 5 | 00001–99999 |
100,000 |
| 6 | 000001–999999 |
1,000,000 |
Pick based on expected volume with headroom. Note that a number wider than number_length is not truncated — it simply grows, so the display width can change at roll-over.
Test Roll-Over
Test what happens at the boundary before it matters:
# With number_length=4, set nextnumber near 9999 and save records.
# Observe whether the value rolls to 10000 (wider) or needs a format change.
Decide in advance whether to widen number_length or introduce a year segment before the limit is reached.
Handle Concurrency
generate_sequence() performs a read (config.nextnumber) followed by a separate config.save(). That read-modify-write is not atomic:
- For a single-server, low-concurrency deployment this is usually fine.
- For concurrent writers, two saves can read the same
nextnumberand produce duplicates.
Mitigations:
- Serialize generation at the database level (row locks /
select_for_update()in a customgenerate_sequence()). - Generate sequences in a dedicated process or queue.
- Consider a database sequence or an atomic
F()expression update in a custom implementation.
See Using SequenceMixin in Models for overriding generate_sequence().
Back Up and Monitor
Backup: include SequenceConfig.nextnumber in your backup/restore strategy. Restoring an older value can re-issue numbers that were already handed out.
Monitor utilization: track how close active sequences are to their limit.
# Admin dashboard helper: warn when a sequence is >90% used
def sequence_health_check():
configs = SequenceConfig.objects.filter(active=True)
for config in configs:
utilization = (config.nextnumber - 1) / (10 ** config.number_length)
if utilization > 0.9:
print(f"Warning: {config} is {utilization:.1%} utilized")
flowchart LR
A["SequenceConfig active"] --> B["utilization =<br/>(nextnumber-1) / 10^length"]
B --> C{"> 90%?"}
C -->|Yes| D["Warn / widen length"]
C -->|No| E["OK"]