Design an Extensible Expense Validation Engine
RipplingSoftware Engineer · Technical ScreenLast updated
Design an object-oriented validation engine that evaluates an expense report against modular, multi-scoped policies and aggregates all policy violations.
Problem Statement
Corporate expense systems must enforce complex company spending policies. Design and implement an extensible expense validation engine that validates an ExpenseReport against a configurable list of validation rules and collects all policy violations found.
Scopes of Validation Rules
Your engine must support three distinct types of validation rules:
- Item-Level Rules: Inspect individual expense items independently (e.g., receipt required if amount exceeds a threshold, maximum single-transaction limit for a category).
- Aggregate / Report-Level Rules: Inspect totals or groupings across the entire report (e.g., total meal expenses per day cannot exceed a daily allowance, overall report total cannot exceed a budget).
- Cross-Category / Relational Rules: Inspect relationships across different expense categories on matching or overlapping dates (e.g., an employee cannot claim both
CAR_RENTALandMILEAGEon the same date).
Your design should adhere to the Open-Closed Principle: adding a new rule should not require modifying the validation engine or existing rules.
Interface
from dataclasses import dataclass
from typing import List, Optional
from datetime import date
from enum import Enum
class ExpenseCategory(Enum):
MEALS = "MEALS"
LODGING = "LODGING"
AIRFARE = "AIRFARE"
CAR_RENTAL = "CAR_RENTAL"
MILEAGE = "MILEAGE"
OTHER = "OTHER"
@dataclass
class ExpenseItem:
id: str
category: ExpenseCategory
amount: float
expense_date: date
has_receipt: bool
description: str = ""
@dataclass
class ExpenseReport:
report_id: str
employee_id: str
items: List[ExpenseItem]
@dataclass
class Violation:
rule_name: str
message: str
item_ids: List[str] # Empty if report-wide
class ValidationEngine:
def __init__(self, rules: List['Rule']):
pass
def validate(self, report: ExpenseReport) -> List[Violation]:
"""Executes all registered rules and returns all accumulated violations."""
pass
Requirements
Implement the engine alongside at least three concrete rules:
ReceiptRequiredRule: Any item in specified categories with anamount > thresholdmust havehas_receipt == True.DailyCategoryCapRule: The sum of expenses for a specific category on any single date cannot exceed a given cap.MutualExclusionRule: Two specified categories (e.g.,CAR_RENTALandMILEAGE) cannot both appear on the exact same date.
Constraints
- An expense report may contain up to
10^4items. - Amounts are positive floats with up to 2 decimal places.
- A rule failure must not halt validation; all applicable violations must be reported.
Example
items = [
ExpenseItem("e1", ExpenseCategory.MEALS, 80.0, date(2025, 3, 1), False),
ExpenseItem("e2", ExpenseCategory.MEALS, 50.0, date(2025, 3, 1), True),
ExpenseItem("e3", ExpenseCategory.CAR_RENTAL, 120.0, date(2025, 3, 1), True),
ExpenseItem("e4", ExpenseCategory.MILEAGE, 45.0, date(2025, 3, 1), True),
]
report = ExpenseReport("rep_1", "emp_1", items)
rules = [
ReceiptRequiredRule(threshold=75.0, categories={ExpenseCategory.MEALS}),
DailyCategoryCapRule(category=ExpenseCategory.MEALS, daily_limit=100.0),
MutualExclusionRule(ExpenseCategory.CAR_RENTAL, ExpenseCategory.MILEAGE)
]
engine = ValidationEngine(rules)
violations = engine.validate(report)
Output: 3 violations:
ReceiptRequiredRuleone1(amount $80 > $75 buthas_receipt == False).DailyCategoryCapRuleforMEALSon2025-03-01($130 > $100 limit, items['e1', 'e2']).MutualExclusionRuleon2025-03-01(conflicting items['e3', 'e4']).
Hints
Hint 1
Define an abstract base `Rule` interface with a `validate(report: ExpenseReport) -> List[Violation]` method so the engine treats all rules uniformly.
Hint 2
For rules requiring grouping by date or category, use hash maps (e.g., `collections.defaultdict`) to pre-aggregate items in linear time before evaluating thresholds.
Hint 3
Ensure each `Violation` clearly identifies the responsible item IDs so downstream consumers can highlight specific lines in the UI.
Follow-up questions
- How would you support rule severity levels (e.g., hard errors blocking submission vs. soft warnings that permit an override)?
- If some rules depend on external data (like an employee's country or department policy lookup), how would you inject context without tightly coupling rules to the database?
- How would you optimize rule execution if some rules are computationally heavy and the report has hundreds of thousands of items?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.