CASE STUDY

Expense Policy and Violation Processing (Rippling)

4 min read·774 words·Advanced

Asked at

2 candidate reports between Feb 2026 and May 2026

How to use this case study

SDE-2 / Mid

Explain how an expense is checked against company rules, what a rule looks like, and how violations are flagged for approvers.

SDE-3 / Senior

Go deeper on only evaluating relevant rules (indexing rules by category and department), month-end spikes, rule changes without downtime, and explainable results.

Staff / Principal

Discuss multi-tenant scale, re-evaluating old expenses when rules change, auditability, and related workflows (driver-pay ledger, termination orchestration).


0) Problem Restatement

Companies set expense policies, such as:

  • "Meals over $75 need a receipt."
  • "No alcohol purchases."
  • "Hotels in New York max $300/night; elsewhere $200."
  • "Flights over $1,000 need manager pre-approval."

When employees submit expenses (often from a corporate card, automatically), each expense must be checked against the applicable rules. Violations are flagged with a clear reason and routed to approvers. At month-end, volume spikes. Rules change often, and each company (tenant) has its own. Rippling asked this, sometimes together with related pieces: a tagged event counter, a driver-pay ledger, and an employee-termination workflow across external systems.


1) Requirements

  • Tenants define rules with conditions (amount, category, merchant, location, employee department or level, receipt present, ...) and actions (flag, block, require approval).
  • Evaluate each new or edited expense against that tenant's active rules.
  • Explain each violation ("Meal $92 > $75 limit for Sales department").
  • Route flagged expenses to approvers, and track resolution.
  • Handle month-end spikes. Rule changes apply without downtime.

1.1 Scale Estimates

  • 50K companies, 50M expenses/month, with a month-end peak of ~2K expenses/sec.
  • Rules per company: 10–500.


2) Rule Representation

Store rules as data, not code:

{
  "rule_id": "r-17", "tenant_id": "acme", "version": 4, "active": true,
  "scope": { "categories": ["meals"], "departments": ["sales", "support"] },
  "condition": { "all": [
      { "field": "amount_usd", "op": ">", "value": 75 },
      { "field": "has_receipt", "op": "==", "value": false } ] },
  "action": { "type": "flag", "severity": "medium",
              "message": "Meals over $75 need a receipt" }
}
  • Scope fields (category, department, country) are used for indexing, meaning quickly narrowing which rules could apply.
  • Condition is a small expression tree (all, any, not, comparisons). It's validated when saved.
  • Rules are versioned, so every decision records which rule version was used.


3) Architecture

Architecture Diagram

flowchart LR
    CARD["Card feed / employee app"] --> EXP["Expense Service"]
    EXP --> K[("Expense events - by tenant")]
    K --> EV["Policy evaluators"]
    EV --> RC[("Compiled rules cache per tenant")]
    ADM["Admin rule editor"] --> RS["Rule Service"]
    RS --> RDB[("Rules DB - versioned")]
    RS -->|"rule changed"| RC
    EV --> VDB[("Violations + decisions")]
    VDB --> APPR["Approval workflow"]
    APPR --> N["Notify approvers"]
  • Expense events go to Kafka, partitioned by tenant, so bursts are absorbed and order is kept per tenant.
  • Evaluators (stateless, autoscaled) load a tenant's compiled rule set from a cache.
  • Rule Service saves new versions, compiles and validates them, and publishes a change event that refreshes caches.


4) Evaluating Efficiently

Checking every rule against every expense is wasteful. Instead:

  1. Index rules by scope per tenant: a map category → rules, department → rules, plus "applies to all". For an expense in "meals" by a "sales" employee, take rules[meals] ∪ rules[all], then keep only those whose department scope includes sales.
  2. Evaluate conditions only for those candidates (usually a handful).
  3. Compile conditions once into fast predicate functions when the rule is saved, not parsed on every evaluation.
  4. Enrich the expense first (the employee's department, city tier, currency converted to USD) so conditions are simple lookups.

This is the same idea as a production rules engine: filter by indexes first, then evaluate. It's also why Rippling's "scale a rules engine for high traffic" question focuses on indexing and caching compiled rules.


5) Key Flows

  1. An expense arrives (card swipe or manual). The service stores it and emits an event.
  2. The evaluator enriches it, finds candidate rules, and evaluates them.
  3. It saves the decision: { expense_id, rule_id, rule_version, result, message }, which makes the result explainable and auditable.
  4. Violations with the "require approval" action create approval tasks. "Block" actions stop reimbursement. "Flag" actions show a warning.
  5. If the employee edits the expense (e.g., adds a receipt), it's re-evaluated.

Rule changes: new expenses use the new version immediately (after the cache refresh). Optionally, re-evaluate open (not yet approved) expenses in a background job. Approved ones stay as they were.

6) Month-End Spikes and Reliability

  • Kafka buffers the spike, and evaluators autoscale on consumer lag.
  • Processing is idempotent: a decision is keyed by (expense_id, expense_version, ruleset_version), so retries don't duplicate violations.
  • One huge tenant can't starve others: use fair scheduling across tenant partitions, or dedicated capacity for very large tenants.


7) Trade-offs & Alternatives

DecisionChoiceWhyAlternative
RulesData (JSON expressions), versionedAdmins edit without deploys, auditableCode per customer: unscalable
MatchingScope indexes, then evaluateFew rules checked per expenseEvaluate all rules: wasteful
ProcessingAsync via KafkaAbsorbs spikesSynchronous on submit: slow at peaks
ExplainabilityStore rule version + message per decisionClear to employees and auditorsJust "violation": confusing

8) Wrap-Up

Represent policies as versioned JSON rules with a scope (for indexing) and a condition tree (compiled when saved). Stream expenses through Kafka partitioned by tenant into autoscaled evaluators that enrich each expense, pick candidate rules via scope indexes, evaluate them, and store explainable, idempotent decisions that drive approval workflows. Refresh compiled rule caches on change, and re-evaluate open expenses when policies change.

More Case Studies

Practice with a Mock Interview

Apply what you learned in a live system design mock interview with our AI interviewer.

Start System Design Interview →