Skip to content
Reliable Data Engineering
Practice problem hard billingexactly-onceledgerscd2reconciliation
Practise with timer, notes and rubric

Design a Usage Metering and Billing Pipeline

Problem

A cloud data platform charges customers for usage: compute seconds, storage GB-hours, API calls and GPU tokens. Services emit usage events. Design the pipeline that turns raw usage into accurate, auditable invoices, near-real-time spend dashboards for customers, and budget alerts. Prices change over time, enterprise customers have negotiated rates, prepaid credits and annual commitments, and finance must be able to explain every cent.


Clarifying questions

QuestionAssumed answer
Volume?~5B usage events/day from ~200 services, peak 150k events/s
Customers?80k accounts; 2k enterprise with custom contracts
Freshness?Customer spend dashboard within 15 min; invoices monthly; budget alerts within 15 min
Correctness?Invoices must be exact and reproducible; dashboards may be approximate (labelled “estimated”)
Late events?Most within minutes; some services batch-upload up to 72 h late
Corrections?Yes: a service can emit corrections (e.g. a bug over-reported usage) and support can issue credits
Currency/tax?Multi-currency; tax handled by a downstream tax service

1. Requirements

Functional: ingest usage events; deduplicate; aggregate per account × SKU × hour; price with the rate effective at usage time (list price, contract overrides, tiers); apply credits and commitments in a defined order; produce invoices and line items; expose spend to customers; budget alerts; support corrections and restatements.

Non-functional: no lost or double-counted usage (effectively-once); reproducible invoices (re-running the month gives the same numbers); a full audit trail; isolation so one noisy service can’t delay everyone; SOX-style controls on pricing changes.

2. Estimates

3. Architecture

flowchart LR
    subgraph SVC[Services]
        S1[Compute] & S2[Storage] & S3[API gateway] --> AGT[Metering SDK<br/>event_id, account, sku, qty, usage_ts]
    end
    AGT --> K[(Kafka: usage-events<br/>key = account_id)]
    K --> RAW[(Bronze: raw usage<br/>append-only, 13-month retention)]
    K --> RT[Streaming aggregation<br/>dedup on event_id, 1-min windows]
    RT --> EST[(Estimated spend<br/>account × sku × hour)]
    EST --> DASH[Customer spend dashboard<br/>+ budget alerts]
    RAW --> HOURLY[Batch: dedup + hourly usage<br/>recompute affected hours]
    HOURLY --> USAGE[(Silver: usage_hourly)]
    PRICE[(Pricing catalog SCD2<br/>list prices, tiers)] --> RATE
    CONTRACT[(Contracts SCD2<br/>overrides, discounts, commits)] --> RATE
    USAGE --> RATE[Rating engine<br/>as-of price lookup]
    RATE --> RATED[(Rated usage lines)]
    RATED --> LEDGER[Billing ledger<br/>charges, credits, commitments]
    LEDGER --> INV[Invoice run<br/>month close + finalisation]
    INV --> OUT[Invoices, line items → payments, tax, ERP]
    INV --> REC[Reconciliation & audit]

Two paths, one source:

4. Data model

erDiagram
    USAGE_HOURLY ||--o{ RATED_LINE : "rated into"
    PRICE_VERSION ||--o{ RATED_LINE : "priced by"
    CONTRACT_VERSION ||--o{ RATED_LINE : "overrides"
    RATED_LINE }o--|| LEDGER_ENTRY : "posted as"
    LEDGER_ENTRY }o--|| INVOICE : "billed on"
    USAGE_HOURLY {
        string account_id PK
        string sku PK
        timestamp hour PK
        decimal quantity
        int event_count
        string source_batch_ids
    }
    PRICE_VERSION {
        string sku PK
        timestamp valid_from PK
        timestamp valid_to
        string currency
        json tiers
    }
    CONTRACT_VERSION {
        string account_id PK
        timestamp valid_from PK
        timestamp valid_to
        json sku_overrides
        decimal commit_amount
    }
    RATED_LINE {
        string line_id PK
        string account_id
        string sku
        timestamp hour
        decimal quantity
        decimal unit_price
        decimal amount
        string price_version
        string rating_run_id
    }
    LEDGER_ENTRY {
        string entry_id PK
        string account_id
        string type "charge | credit | commit_drawdown | adjustment"
        decimal amount
        string reference
        timestamp posted_at
    }
    INVOICE {
        string invoice_id PK
        string account_id
        date period
        string status "draft | final | void"
        decimal total
    }

Key choices: money as decimals (or integer micro-units), never floats; effective-dated (SCD2) prices and contracts; an append-only ledger where corrections are new entries (adjustments), never edits.

5. Deep dives

5.1 Effectively-once metering

5.2 Late events and the finalisation window

5.3 Rating with historical prices

5.4 Credits, commitments and ordering

5.5 Corrections and restatements

5.6 Reconciliation and controls

6. Trade-offs

DecisionChoiceAlternative
Real-time vs exactTwo paths: estimated streaming + exact batchStreaming-only exact billing (harder late-data and correction handling)
Late data72 h finalisation, later usage on the next invoiceReopen invoices (confusing for customers, breaks accounting)
CorrectionsAppend-only adjustmentsIn-place updates (no audit trail)
Money typeDecimal / integer micro-unitsFloats (rounding drift across billions of lines)
Tiered pricingMonthly running totals per account×SKUPer-event tier lookup (wrong at tier boundaries)

7. Failure modes

8. What separates a senior answer

9. Follow-up questions

A customer disputes a $40k line item. How do you explain it?

From the invoice line, follow rating_run_id, price_version and contract_version to the rated lines, then to the hourly usage rows and their source batch IDs, then to the raw events in bronze (13-month retention). Produce a usage breakdown by resource and hour with the price applied. Because everything is versioned and append-only, the explanation is reproducible.

How would you support real-time prepaid balance enforcement (stop service at $0)?

That’s an operational, low-latency path: keep a balance per account in a strongly consistent store, decremented by the streaming estimated-spend aggregator with a safety margin, and have services check it on admission. The batch ledger remains authoritative and reconciles the balance daily; differences are corrected via adjustments.

Pricing moves from per-hour to per-second granularity. What changes?

Event volume per resource goes up unless services pre-aggregate; the hourly usage table can stay hourly (summing seconds), but rating must apply minimum charges and rounding rules at the new granularity. Version the pricing model and apply it by effective date so old periods keep the old rules.


Self-assessment rubric