Requirements
- Input expenses are dictionaries with string keys and values. Typical fields include
expense_id,trip_id,amount_usd,expense_type,vendor_type, andvendor_name. - Implement
evaluate_rules(rules: list<rule>, expenses: list<expense>) -> ...; the return type is part of the design discussion, but it should preserve which expense or trip violated which rule. - Base individual-expense rules include:
- no restaurant expense over $75 where
vendor_type == "restaurant"; - no airfare expenses;
- no entertainment expenses;
- no single expense over $250.
- no restaurant expense over $75 where
- Base group / trip rules include:
- no trip over $2000 total;
- no meal expenses over $200 total per trip.
- Design for future rule types and API-created rules. Rules should be treated as data rather than hardcoded as one function per policy.
- Reuse the same predicate / condition layer across individual and group rules. For example, the
expense_type == "meals"condition can filter expenses before summing a trip-level amount. - Follow-up: handle millions of expenses per day and tens of thousands of rules; discuss storage, rule indexing, streaming evaluation, and notification of violations.
- Follow-up: support composite rules such as
(restaurant AND meals AND amount > 50),(entertainment AND amount > 100) OR client_hosting, and(amount > 100) AND NOT vendor_name == Staples.
Examples
{
"expense_id": "001",
"trip_id": "001",
"amount_usd": "49.99",
"expense_type": "client_hosting",
"vendor_type": "restaurant",
"vendor_name": "Outback Roadhouse"
}
A useful violation payload is explicit enough for API clients and notifications:
{
"rule_id": "single-expense-limit",
"expense_id": "004",
"trip_id": "002",
"message": "Expense 004 exceeds $250"
}
Notes
- A good return type separates individual-expense violations from trip-level violations and preserves which rule was violated.
- The OOD signal is the core of the round: use a predicate / rule interface, then compose rules into an expression tree for AND / OR / NOT. The canonical shape is the Rules / Specification pattern: a
Ruleinterface with a singleevaluate(context) -> boolorevaluate(context) -> Violation?method, plus composite rulesAndRule,OrRule, andNotRulethat hold child rules and combine their results. - Split the type hierarchy into two layers: per-transaction rules that read one expense, and per-trip aggregate rules that read a grouped list. Aggregate rules need an explicit grouping step in the evaluator; running them per expense would either double-count or miss trip totals.
- A practical data-driven model has
Condition(field, operator, value)for leaf predicates andRule(rule_id, condition, violation_message)for individual checks. Group rules addgroup_by,aggregate,threshold, and an optionalfilter_conditionthat reuses the same condition object from Part 1. - When both individual and group rules run, combine results into one response rather than short-circuiting after the first violation; interviewers usually expect all violated policies to be visible.
- For scale, pre-index rules by the fields they read so each expense only triggers the rules whose predicates touch its keys; aggregate per-trip metrics incrementally instead of re-scanning; stream evaluation per trip-window; and emit violation events to an async notification path rather than blocking ingestion.
- Edge cases: parse
amount_usdas numeric before comparing; handle missing fields consistently; decide whether negative or zero amounts are invalid input or simply fail no threshold rule; clarify whether multiple rules can produce duplicate-looking messages for the same expense. - In AI-assisted rounds, interviewers expect the candidate to own the rule interface and the per-trip aggregation contract before prompting the tool. Generating composite-rule boilerplate is fine, but the interface design has to come from you.
Preparation
- Implement the base rule engine twice: once with simple per-expense functions, once with
Ruleobjects +AndRule/OrRule/NotRulecomposites so the same evaluator handles both flat and nested rules. - Group expenses by
trip_idonce per evaluation pass; precompute trip totals and per-expense_typesums and feed them to aggregate rules so the same totals are not recomputed per rule. - Prepare a layered scale answer: rule storage, rule compilation into composite trees on load, field-based indexing of leaf predicates, streaming per-trip evaluation, and violation events emitted to a queue for notifications.

