Invoice Service: Overview
An invoice service manages the full lifecycle of invoices: creation, line items, tax, PDF generation, payment collection, reminders, and credit notes for billing workflows.
Core Data Model
Invoice Table
Invoice (
id BIGINT PRIMARY KEY,
customer_id BIGINT NOT NULL,
subscription_id BIGINT NULL,
status ENUM('draft','open','paid','void','uncollectible'),
subtotal_cents BIGINT NOT NULL,
tax_cents BIGINT NOT NULL,
total_cents BIGINT NOT NULL,
due_date DATE,
paid_at TIMESTAMP NULL,
pdf_s3_key VARCHAR(500),
invoice_number VARCHAR(50),
currency CHAR(3) DEFAULT 'USD',
created_at TIMESTAMP
)
InvoiceLine Table
InvoiceLine (
id BIGINT PRIMARY KEY,
invoice_id BIGINT NOT NULL,
description TEXT,
quantity DECIMAL(10,4),
unit_amount_cents BIGINT,
amount_cents BIGINT,
tax_rate_id BIGINT NULL
)
Invoice Lifecycle
- draft: Created but not yet sent. Line items can be modified.
- open: Finalized and sent to customer. Awaiting payment. No further edits.
- paid: Payment received and applied.
paid_attimestamp recorded. - void: Cancelled before payment. No money collected. Replaced by corrected invoice if needed.
- uncollectible: Marked as bad debt after dunning exhausted.
Transition rules: draft → open (on finalize/send); open → paid (on payment); open → void (on cancellation); open → uncollectible (after dunning failure).
PDF Generation
- Render HTML invoice template with customer details, line items, tax breakdown, and totals.
- Pass HTML to Puppeteer (headless Chrome) to produce a PDF binary.
- Upload PDF to S3 under a structured key:
invoices/{customer_id}/{invoice_number}.pdf - Store the S3 key in
pdf_s3_key. Generate a pre-signed URL for download on demand.
PDF generation runs asynchronously via a job queue to avoid blocking the API response.
Payment Links
InvoicePaymentLink (
id BIGINT PRIMARY KEY,
invoice_id BIGINT NOT NULL,
token VARCHAR(64) UNIQUE, -- random secure token
expires_at TIMESTAMP,
used_at TIMESTAMP NULL
)
Customers click a unique URL containing the token to pay without logging in. Token is single-use; mark used_at on payment. Expiry enforced for security.
Reminder Emails
A scheduler checks open invoices daily and sends reminders at fixed offsets from due_date:
- due_date – 7 days: Friendly upcoming payment reminder.
- due_date (day of): Payment due today notice.
- due_date + 3 days: Overdue notice with payment link.
Track which reminders have been sent in an InvoiceReminder table to prevent duplicates.
Credit Notes
CreditNote (
id BIGINT PRIMARY KEY,
invoice_id BIGINT NOT NULL,
amount_cents BIGINT NOT NULL,
reason TEXT,
created_at TIMESTAMP
)
Credit notes are issued against paid invoices for full or partial refunds or adjustments. The credit amount is either applied to the customer's account balance (reducing the next invoice total) or refunded to the original payment method, depending on business policy.
Invoice Numbering
Sequential numbers per customer in a human-readable format: INV-2026-00001, INV-2026-00002, etc. Use a per-customer sequence (or a global sequence with customer prefix) stored in a CustomerInvoiceSequence table. Increment atomically to avoid gaps or duplicates.
Multi-Currency
Store amounts in the customer's billing currency (currency field). Also store a usd_total_cents using the exchange rate at invoice creation time for reporting. Lock in the rate at finalization; do not re-convert later.
Key Design Considerations
- Immutability: Once an invoice is open, do not mutate it. Issue a credit note or void and reissue instead.
- Tax separation: Delegate tax calculation to a dedicated tax service; store results in InvoiceLine rows.
- Idempotency: Invoice creation from billing run must be idempotent — check if invoice exists for a given period before creating.
- Audit trail: Log all status transitions with actor and timestamp in an
InvoiceEventtable.
{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “What are the main statuses in an invoice lifecycle?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “An invoice moves through draft (editable, not yet sent), open (finalized and sent to the customer awaiting payment), paid (payment received), void (cancelled before payment), and uncollectible (marked as bad debt after dunning is exhausted). Transitions are one-directional and enforced by business logic.”
}
},
{
“@type”: “Question”,
“name”: “How is PDF generation handled for invoices at scale?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “PDF generation runs asynchronously via a job queue. The system renders an HTML invoice template, passes it to Puppeteer (headless Chrome) to produce a PDF, uploads the result to S3 under a structured key, and stores the S3 key in the invoice record. Pre-signed URLs are generated on demand for customer downloads.”
}
},
{
“@type”: “Question”,
“name”: “What is a credit note and when is it used?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “A credit note is issued against a paid invoice to record a full or partial refund or adjustment. The credit amount is either applied to the customer's account balance to reduce their next invoice, or refunded to the original payment method. Credit notes preserve invoice immutability by creating a separate record rather than modifying the original invoice.”
}
},
{
“@type”: “Question”,
“name”: “How do payment links allow customers to pay invoices without logging in?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “When an invoice is finalized, the system generates a cryptographically random token stored in an InvoicePaymentLink table with an expiry time. The customer receives a URL containing this token. Visiting the URL authenticates via the token alone, allowing payment without a login session. The token is marked as used on successful payment to prevent reuse.”
}
}
]
}
See also: Stripe Interview Guide 2026: Process, Bug Bash Round, and Payment Systems
See also: Atlassian Interview Guide
See also: Shopify Interview Guide
See also: Coinbase Interview Guide