2 min read·Run it here

Design a Usage Portal

The screen that starts with a repo already written: an event schema, a seeded fixture, and a plan catalog. You add metering, billing windows, a leaderboard, and overage in whole cents.

Pattern
Event log -> account/metric index
Time
O(1) record, O(n) window
Space
O(events)

Most drills hand you an empty class. This one hands you a codebase: the event schema, a seeded fixture, and a plan catalog are already in the file. That is the real shape of a metering screen — you are reading someone else's types before you write a line.

The recipe

Say before you type: "Keep the event log. The totals map is an index over it, not a replacement for it."

Two structures, and the second one is the trap:

private events: UsageEvent[] = [];
private totals = new Map<string, Map<Metric, number>>();

Running totals answer totalFor in O(1) and feel like the clean choice. They cannot answer how much between Monday and Wednesday at all. An implementation that only kept counters has already thrown that data away by the time phase two asks. Keep the log; index it for speed.

Dedupe belongs in record, and it is one line:

if (this.seen.has(event.id)) return;

The meter delivers at least once. Replays are normal traffic, not an error path.

Half-open windows

State the convention out loud before you write the comparison:

event.at >= startMs && event.at < endMs

[start, end) means back-to-back periods tile the month with no double billing. An inclusive end double-counts every boundary event, and boundary events are exactly the ones a customer disputes.

The leaderboard

topAccounts is sort-then-slice, and the tie-break is the graded part:

rows.sort((a, b) => b.total - a.total || (a.accountId < b.accountId ? -1 : 1));

Highest total first, account id ascending on ties. A comparator with no tie-break is not wrong on the happy path — it is unspecified, and a dashboard that reshuffles equal rows on refresh reads as a bug.

Money is integers

Quantities are fractional. Money is not. Take the overage per metric, multiply by that metric's rate, round to a whole cent, then sum:

cents += Math.round(over * plan.rateCents[metric]);

Rounding once at the end gives a different total. Say which one you picked and why — per-metric rounding is what a line-item invoice shows.

null in the catalog means unlimited: remaining returns Infinity and that metric bills nothing. And ask the scope question before you write overageCents — this portal bills one period, so quota applies to everything recorded.