Katkat

Design an Extensible Expense Validation Engine

RipplingSoftware Engineer · Technical Screen

Last 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:

  1. Item-Level Rules: Inspect individual expense items independently (e.g., receipt required if amount exceeds a threshold, maximum single-transaction limit for a category).
  2. 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).
  3. Cross-Category / Relational Rules: Inspect relationships across different expense categories on matching or overlapping dates (e.g., an employee cannot claim both CAR_RENTAL and MILEAGE on 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:

  1. ReceiptRequiredRule: Any item in specified categories with an amount > threshold must have has_receipt == True.
  2. DailyCategoryCapRule: The sum of expenses for a specific category on any single date cannot exceed a given cap.
  3. MutualExclusionRule: Two specified categories (e.g., CAR_RENTAL and MILEAGE) cannot both appear on the exact same date.

Constraints

  • An expense report may contain up to 10^4 items.
  • 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:

  • ReceiptRequiredRule on e1 (amount $80 > $75 but has_receipt == False).
  • DailyCategoryCapRule for MEALS on 2025-03-01 ($130 > $100 limit, items ['e1', 'e2']).
  • MutualExclusionRule on 2025-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?