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.

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.

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