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.
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