Low Level Design: Expense Management System

An expense management system handles the full lifecycle of employee expenses: from receipt capture in the field to reimbursement hitting a bank account. Interviewers use this problem to probe your ability to design multi-step workflows, file handling, and policy enforcement at scale.

Functional Requirements

  • Employees submit expense reports with receipts attached.
  • OCR extracts merchant, date, amount, and currency from receipt images.
  • Policy engine validates expenses against company rules (limits, categories, required fields).
  • Approval workflow routes reports to managers based on amount thresholds.
  • Finance team processes approved reports and triggers reimbursement.
  • Employees and managers can query expense status and history.

Non-Functional Requirements

  • OCR processing is asynchronous and must complete within 30 seconds.
  • Audit trail is immutable and retained for 7 years.
  • System supports multiple currencies with real-time FX conversion.
  • Multi-tenant: each company has isolated policy configuration.

Core Entities

ExpenseReport
  id UUID PK
  employee_id UUID FK
  company_id UUID FK
  title VARCHAR(255)
  status ENUM('DRAFT','SUBMITTED','PENDING_APPROVAL','APPROVED','REJECTED','REIMBURSED')
  total_amount DECIMAL(12,2)
  currency CHAR(3)
  submitted_at TIMESTAMP
  created_at TIMESTAMP

ExpenseLineItem
  id UUID PK
  report_id UUID FK
  category_id UUID FK
  merchant VARCHAR(255)
  amount DECIMAL(12,2)
  currency CHAR(3)
  expense_date DATE
  receipt_id UUID FK
  policy_status ENUM('PENDING','COMPLIANT','VIOLATION','OVERRIDE')
  notes TEXT

Receipt
  id UUID PK
  line_item_id UUID FK
  storage_key VARCHAR(512)
  ocr_status ENUM('PENDING','PROCESSING','DONE','FAILED')
  ocr_result JSONB
  uploaded_at TIMESTAMP

ApprovalStep
  id UUID PK
  report_id UUID FK
  approver_id UUID FK
  level INT
  status ENUM('PENDING','APPROVED','REJECTED','SKIPPED')
  decided_at TIMESTAMP
  comment TEXT

PolicyRule
  id UUID PK
  company_id UUID FK
  category_id UUID FK
  daily_limit DECIMAL(12,2)
  per_item_limit DECIMAL(12,2)
  requires_receipt_above DECIMAL(12,2)
  requires_justification BOOLEAN

Receipt Capture and OCR Pipeline

Receipt upload is a two-step process. First the client gets a pre-signed S3 URL and uploads directly to object storage. The API then records the storage key and enqueues an OCR job.

POST /receipts/upload-url
  -> { upload_url, receipt_id }

PUT {upload_url}   // client uploads directly to S3

POST /receipts/{receipt_id}/confirm
  -> enqueue OCRJob(receipt_id)
  -> Receipt.ocr_status = PROCESSING

The OCR worker polls the queue, downloads the image from S3, calls the OCR provider (Google Document AI, AWS Textract, or internal model), and stores structured output back to the Receipt row. It then publishes an ocr.completed event that pre-fills the line item form for the employee to review before submission.

Policy Compliance Checking

Policy validation runs at two points: on-save for immediate feedback and again at submission to prevent race conditions if rules changed.

class PolicyEngine:
  def validate(line_item, policy_rules):
    violations = []
    rule = policy_rules.get(line_item.category_id)
    if rule:
      if line_item.amount > rule.per_item_limit:
        violations.append(PolicyViolation(
          type='LIMIT_EXCEEDED',
          limit=rule.per_item_limit,
          actual=line_item.amount
        ))
      if line_item.amount > rule.requires_receipt_above and not line_item.receipt_id:
        violations.append(PolicyViolation(type='RECEIPT_REQUIRED'))
    return violations

Violations do not block submission by default. They are surfaced to the approver and can require manager override. Hard blocks (e.g. amount exceeds absolute cap) are configurable per company.

Approval Workflow

When a report is submitted, the workflow engine builds an approval chain based on the total amount and the employee's org chart position.

function buildApprovalChain(report, employee):
  steps = []
  if report.total_amount > 0:
    steps.append(ApprovalStep(approver=employee.manager, level=1))
  if report.total_amount > company.finance_review_threshold:
    steps.append(ApprovalStep(approver=finance_team_queue, level=2))
  return steps

// On each approval:
function advance(report):
  next = steps.filter(status='PENDING').first()
  if not next:
    report.status = 'APPROVED'
    emit('report.approved', report.id)
  else:
    notify(next.approver)

Reimbursement Processing

Approved reports are batched for reimbursement. A nightly job aggregates all reports approved since the last run, groups by employee bank account, and submits a bulk ACH/SEPA file to the payment provider. Status callbacks update the report to REIMBURSED and notify the employee.

ReimbursementBatch
  id UUID PK
  run_at TIMESTAMP
  status ENUM('PENDING','SUBMITTED','SETTLED','FAILED')
  report_ids UUID[]
  total_amount DECIMAL(14,2)
  provider_reference VARCHAR(255)

Reporting and Analytics

Finance dashboards query a read replica or a dedicated OLAP table populated by a CDC pipeline. Common queries: spend by department/category/period, policy violation rate, average approval cycle time. Aggregate tables are refreshed hourly to avoid heavy joins on the transactional database.

API Surface

POST   /expense-reports                    // create draft
POST   /expense-reports/{id}/line-items    // add line item
DELETE /expense-reports/{id}/line-items/{lid}
POST   /expense-reports/{id}/submit        // trigger policy check + workflow
GET    /expense-reports/{id}               // status, violations, steps
POST   /approvals/{step_id}/approve
POST   /approvals/{step_id}/reject
GET    /reports/spend-summary?period=&group_by=

Key Design Decisions

  • Async OCR: Keeps upload fast; employees can submit manually if OCR fails.
  • Policy at two checkpoints: Prevents stale validation if rules change between save and submit.
  • Immutable audit log: Separate append-only table tracks every status transition with actor and timestamp.
  • Idempotent reimbursement: Batch IDs prevent double-payment if the ACH submission is retried.
  • FX snapshot: Store the exchange rate at submission time, not at reimbursement time, to avoid disputes.

Frequently Asked Questions

What is an expense management system?

An expense management system is a platform that automates the end-to-end lifecycle of employee spending: capturing receipts, enforcing spend policies, routing approval workflows, and disbursing reimbursements. It replaces manual spreadsheet submissions with structured data capture and audit trails. Core entities are expenses, receipts, policies, and reimbursement batches. At scale, these systems integrate with ERP and payroll platforms so finance teams get real-time visibility into operating costs without manual reconciliation.

How does receipt OCR work in expense systems?

When an employee uploads a photo of a receipt, the system passes the image through an OCR pipeline — typically a cloud vision API (Google Vision, AWS Textract, or a fine-tuned model) — to extract structured fields: merchant name, date, total amount, currency, and line items. The extracted fields are mapped onto an expense record as draft values. A confidence score is attached to each field; low-confidence fields are flagged for manual review. The raw image is stored in object storage and its URL is persisted alongside the expense record so auditors can verify the original. Duplicate detection runs a perceptual hash or embedding similarity check against previously submitted receipts to catch re-submissions.

How does policy compliance enforcement work in an expense system?

Policy rules are expressed as configurable predicates — for example, “meal expenses above $75 require a manager approval” or “airfare must be economy class unless travel is longer than 6 hours.” When an expense is submitted, a rules engine evaluates the expense attributes (category, amount, merchant, travel duration) against the active policy set for that employee’s department and role. Violations are classified as hard blocks (submission rejected outright) or soft flags (submission proceeds but is routed for additional approval). Policy versions are timestamped and linked to each expense so audits can reconstruct which rules were in effect at submission time.

How does the reimbursement flow work in an expense system?

After an expense clears approval, it enters a reimbursement queue. A batch job aggregates approved expenses per employee on a pay cycle cadence (weekly or biweekly). The batch computes a net reimbursable amount, deducts any corporate card charges already settled, and generates a payment instruction. Payment is disbursed via ACH, payroll integration, or direct bank transfer depending on the configured payout method. Each disbursement produces a remittance record linking the payment transaction ID back to the individual expense IDs, giving employees an itemized statement and giving finance a reconcilable ledger entry. Failed disbursements trigger a retry with exponential backoff and alert the finance team after a configurable number of failures.

{ “@context”: “https://schema.org”, “@type”: “FAQPage”, “mainEntity”: [ { “@type”: “Question”, “name”: “What is an expense management system?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “An expense management system is a platform that automates the end-to-end lifecycle of employee spending: capturing receipts, enforcing spend policies, routing approval workflows, and disbursing reimbursements. It replaces manual spreadsheet submissions with structured data capture and audit trails.” } }, { “@type”: “Question”, “name”: “How does receipt OCR work in expense systems?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Receipt images are passed through an OCR pipeline to extract structured fields such as merchant name, date, total amount, and currency. Extracted fields populate a draft expense record with confidence scores; low-confidence fields are flagged for review. The raw image is stored in object storage for audit purposes.” } }, { “@type”: “Question”, “name”: “How does policy compliance enforcement work in an expense system?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Policy rules are configurable predicates evaluated against expense attributes at submission time. Violations are classified as hard blocks or soft flags requiring additional approval. Policy versions are timestamped and linked to each expense for audit purposes.” } }, { “@type”: “Question”, “name”: “How does the reimbursement flow work in an expense system?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Approved expenses are batched per employee on a pay cycle cadence. A net reimbursable amount is computed and disbursed via ACH or payroll integration. Each disbursement links back to individual expense IDs for reconciliation, with retries and alerts for failed payments.” } } ] }

See also: Stripe Interview Guide 2026: Process, Bug Bash Round, and Payment Systems

See also: Shopify Interview Guide

See also: Atlassian Interview Guide

See also: Databricks Interview Guide 2026: Spark Internals, Delta Lake, and Lakehouse Architecture

See also: LinkedIn Interview Guide 2026: Social Graph Engineering, Feed Ranking, and Professional Network Scale

Scroll to Top