Low Level Design: Invoice Service

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_at timestamp 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

  1. Render HTML invoice template with customer details, line items, tax breakdown, and totals.
  2. Pass HTML to Puppeteer (headless Chrome) to produce a PDF binary.
  3. Upload PDF to S3 under a structured key: invoices/{customer_id}/{invoice_number}.pdf
  4. 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 InvoiceEvent table.

{
“@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

Scroll to Top