A customer changes their billing address. You update the customer record, reopen an invoice issued last year and discover that the old invoice now displays the new address.
Nothing failed at the database level. The application simply asked the wrong question: “What are this customer’s details now?” instead of “What details were recorded on this issued document?”
An issued invoice should preserve the facts used when it was issued. Keeping a foreign key to the customer is useful, but that relationship alone does not preserve the document’s history.
Historical customer and supplier information is a documented concern in Fracture. The models in this article are illustrative design options, not a disclosure of that project’s actual database schema. This is technical guidance, not a certification of compliance with any country’s invoicing rules.
Separate the customer from the document
The customer record represents an entity whose current details can change. The invoice is a record of a specific business transaction. These objects have different lifecycles.
A simplified model makes the distinction explicit:
customer
id
current_legal_name
current_tax_identifier
current_billing_address
invoice
id
customer_id
status
issued_at
buyer_name_snapshot
buyer_tax_identifier_snapshot
buyer_address_snapshot
seller_name_snapshot
seller_tax_identifier_snapshot
seller_address_snapshot
currency
total
invoice_line
invoice_id
description_snapshot
quantity
unit_price
discount
tax_rate
tax_amount
line_total
The foreign key supports navigation and reporting. The snapshots support document rendering. Editing a customer’s current details should not trigger a rewrite of issued invoice snapshots.
Product descriptions and prices deserve the same treatment. Rendering an old invoice by joining the current product catalogue can silently change the meaning or amount of the transaction.
Decide when the snapshot becomes authoritative
A draft is not an issued document. It may be reasonable for a draft to refresh customer details, but the interface must explain that behavior and show what will be issued.
One defensible workflow is:
- Create an editable draft with references to the customer and products.
- Resolve the intended billing details and calculate totals for review.
- At issuance, validate the final values and persist the snapshots, line amounts, document identifier and issue time atomically.
- Render the issued document from those stored values, not from live master data.
This is a policy choice, not the only possible workflow. If your product freezes details earlier, record and communicate that point explicitly. Avoid an accidental mixture where the address is copied at draft creation but the company name is looked up at download time.
Treat issuance as a transaction
Two requests can attempt to issue the same draft concurrently. A UI-disabled button does not protect against retries or direct API requests.
Use a database transaction with an appropriate lock or conditional update so only one valid draft-to-issued transition succeeds. Allocate and validate the document identifier according to the application’s actual numbering requirements. Persist the final line values and invoice totals in the same transaction.
Define how concurrent changes to customer details are handled too. The review screen may show version 4 while another user saves version 5. You can reject issuance until the new details are reviewed, or issue the explicitly reviewed snapshot if that fits the product’s rules. Silently mixing versions is the failure to avoid.
Use exact numeric types or another explicitly defined exact representation for financial values. Persist the rounding decisions that produced the issued totals. Recalculating an old document with a newer rounding policy can change it even when the customer snapshots are intact.
Preserve the rendered document when necessary
Stable source data does not guarantee an identical future PDF. Fonts, templates, translations and rendering libraries can change.
If the application needs to reproduce the exact issued artifact, retain the generated document in controlled storage and record its identifier and checksum. A checksum can help detect an accidental change, but a hash stored beside a file is not independently tamper-proof evidence.
Document generation also introduces a failure boundary. Decide what happens if the database commits but PDF creation fails. A durable generation job can allow retries, but the interface should distinguish an issued record with a pending artifact from a document ready to send. Some workflows require a different issuance boundary; make that requirement explicit before implementing it.
Corrections should not silently rewrite history
Locking editing in the UI is only the first layer. Enforce issued-document restrictions on server-side write paths, including imports, administrative tools and background jobs.
When an issued invoice is wrong, use the correction workflow required for the relevant jurisdiction and business context. It may involve a linked corrective document or another auditable process. Do not silently change the original and leave recipients holding inconsistent versions.
An audit trail should identify who performed an operation, when it happened and which document was affected. Restrict access and avoid keeping unrelated personal data “just in case.” Retention and deletion requirements need legal review; immutability is not an excuse to retain everything forever.
A regression test that catches the original bug
Start with a small, explicit scenario:
Given a customer named "Example Workshop" at "10 North Street"
And an issued invoice containing those buyer details
When the customer's address changes to "25 South Street"
Then the customer profile shows "25 South Street"
And the issued invoice still shows "10 North Street"
And a new draft follows the documented refresh policy
Extend it to cover seller-detail changes, product renaming, tax and rounding changes, duplicate issue requests and concurrent customer edits. Try updating an issued record through the API, not just through the form.
Also test artifact generation failure and recovery. The application should not create a second invoice just because the first PDF generation attempt timed out.
Snapshot or full event history?
Snapshots are often the smallest solution when the requirement is to reproduce an issued document. A versioned master-data model or event history can support richer historical queries, but it introduces additional reconstruction and migration responsibilities.
Choose based on the queries and guarantees you need. As with scalable system design, the goal is not to adopt the most elaborate pattern. It is to make the important behavior stable, observable and testable.