TL;DR: Ask three questions before writing anything: are partial hours billed proportionally or rounded up to the hour, what rounding rule applies to the final amount, and what should happen to a malformed record. Then the implementation is short. Use a decimal type rather than a float, because binary floating point cannot represent 0.1 exactly and repeated additions of cents accumulate error that eventually shows up as a mismatched invoice. Compute duration from timestamps in seconds and divide, so a 7.5-minute allocation of four A100s at $1.10 an hour comes to $0.55 rather than being rounded away. Validate each record: reject an unknown GPU type and reject an interval whose end is not after its start, both loudly, because a silent zero in a billing pipeline is worse than a crash. The tests that matter are the partial hour, the month boundary, the malformed record, and the free-credit floor, since a tenant whose usage is below their credit must owe zero rather than a negative amount.
How to approach it
Ask the three product questions first, because they change the code and an interviewer is checking whether you ask or assume. State the decimal decision with its reason. Write the small, boring, correct version. Then name the tests, since for a billing problem the tests are half the answer and the interesting ones are all edge cases.
A strong answer
A typical situation: a billing pipeline built with floats reconciles to within a few cents most months, and once a quarter a large tenant's invoice differs from the usage export by an amount somebody has to explain. The arithmetic was never wrong in a way anyone could point at, which is exactly the problem with accumulating floating-point error across millions of records.
The three questions:
1. partial hours: proportional, or rounded up to the whole hour?
proportional is what most cloud billing does and what this implementation assumes
rounding up is a real policy and it changes the code and roughly doubles small-allocation
revenue, so it is a product decision rather than an implementation detail
2. rounding: to what precision, and which rule?
cents, half-up, applied once at the end of each record's cost
applying it per record rather than per tenant means the sum of line items equals the total,
which is what a customer will check
3. malformed records: reject or skip?
reject loudly. A billing pipeline that silently skips records under-bills, and nobody
notices until an audit
The implementation:
from dataclasses import dataclass
from datetime import datetime, timezone
from decimal import Decimal, ROUND_HALF_UP
RATES = {"h100": Decimal("2.50"), "a100": Decimal("1.10"), "l4": Decimal("0.35")}
@dataclass(frozen=True)
class Usage:
tenant: str
gpu_type: str
count: int
start: datetime
end: datetime
def hours(u: Usage) -> Decimal:
if u.end <= u.start:
raise ValueError(f"end {u.end} not after start {u.start}")
secs = Decimal((u.end - u.start).total_seconds())
return secs / Decimal(3600)
def cost(u: Usage) -> Decimal:
try:
rate = RATES[u.gpu_type]
except KeyError:
raise ValueError(f"unknown gpu type {u.gpu_type!r}") from None
raw = rate * Decimal(u.count) * hours(u)
return raw.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
def bill(records, free_credit=Decimal("0")):
totals = {}
for u in records:
totals[u.tenant] = totals.get(u.tenant, Decimal("0")) + cost(u)
return {t: max(Decimal("0"), v - free_credit).quantize(Decimal("0.01"))
for t, v in totals.items()}
Running it on three records:
acme h100 x8 for 6.5 h -> $130.00
acme a100 x4 for 0.125 h -> $0.55
globex l4 x1 for 696 h -> $243.60
bill with $50 free credit: {'acme': '80.55', 'globex': '193.60'}
partial-hour check: 4 x $1.10 x 0.125 h = $0.55, which is the case a rounded-up
implementation would have charged $4.40 for
unknown type rejected: unknown gpu type 'h200'
reversed interval rejected: end 2026-09-01 00:00:00+00:00 not after start 2026-09-01 01:00:00+00:00
The decimal decision, which is the one an interviewer is listening for:
why not float
binary floating point cannot represent 0.1 exactly, so 0.1 + 0.2 != 0.3
a single record's error is far below a cent and invisible
a million records summed accumulate error that can reach cents or more, and the direction
is not predictable
the failure mode: the invoice and the usage export disagree by an amount nobody can locate,
which is the scenario above
why decimal
exact decimal representation of the values money is actually denominated in
explicit rounding at a chosen precision with a chosen rule, rather than whatever the
hardware does
slower, by a large factor, and entirely irrelevant here since this is not a hot loop
the general rule: money is decimal, and if performance ever makes that impossible, use
integer cents rather than float
sanity: one record's float error is around 1e-16 relative, so a single invoice is fine and a
year of 10 million records is not. The cost of decimal here is a few microseconds per
record against a pipeline that runs once a month, so there is nothing to trade
The tests, which are the other half of the answer:
def test_partial_hour():
u = Usage("t", "a100", 4, dt("2026-09-01T10:00:00"), dt("2026-09-01T10:07:30"))
assert cost(u) == Decimal("0.55") # 4 x 1.10 x 0.125
def test_month_boundary():
# a record spanning midnight on the last day belongs to whichever period the
# policy says; the function must not silently split or drop it
u = Usage("t", "h100", 1, dt("2026-08-31T23:00:00"), dt("2026-09-01T01:00:00"))
assert cost(u) == Decimal("5.00")
def test_unknown_type_raises():
with pytest.raises(ValueError):
cost(Usage("t", "h200", 1, dt("2026-09-01T00:00:00"), dt("2026-09-01T01:00:00")))
def test_reversed_interval_raises():
with pytest.raises(ValueError):
cost(Usage("t", "h100", 1, dt("2026-09-01T01:00:00"), dt("2026-09-01T00:00:00")))
def test_free_credit_floors_at_zero():
u = Usage("t", "l4", 1, dt("2026-09-01T00:00:00"), dt("2026-09-01T01:00:00")))
assert bill([u], free_credit=Decimal("50")) == {"t": Decimal("0.00")}
def test_line_items_sum_to_total():
# rounding per record means the sum of what the customer sees equals what they pay
assert sum(cost(u) for u in RECORDS) == sum(bill(RECORDS).values())
The month-boundary test deserves a note. A record spanning the boundary has to be handled by an explicit policy: attribute it to the period it started in, to the one it ended in, or split it. All three are defensible and the code must do one of them deliberately, because the case arrives every month and a function that has not decided will do something arbitrary.
The GPU Credit Scheduler Pattern covers the wider system this sits in, including where the usage records come from and how allocation events become intervals.
The reversal condition: this shape assumes usage arrives as clean, paired allocation intervals. Real event streams give start and stop events that can be unpaired, duplicated or out of order, and reconstructing intervals from them is the harder half of the problem. If the interviewer's input is an event log rather than intervals, the first work is pairing and validating, and Interval Merging and Utilization Logs is where that lives. Ask which you are being given before writing either.
What interviewers probe next
- "What if a record is duplicated?" Deduplicate on an identifier before billing. Without one, exact duplicates are indistinguishable from a tenant running the same shape twice on purpose, which is a data-model problem rather than a code one.
- "How would you handle a rate change mid-month?" Rates become time-ranged, and a record spanning a change splits at the boundary. That is why rates belong in a versioned table rather than in a constant.
- "Why round per record?" So the line items a customer sees sum to the total they pay. Rounding once at the end produces a total that does not match the itemization.
- "How do you handle time zones?" Store and compute in a single timezone, and convert only for display. Billing periods are defined in a stated zone and that definition belongs in the configuration.
Common mistakes
- Floats for money, which accumulates error that surfaces as an unexplainable invoice discrepancy.
- Silently skipping malformed records, which under-bills without any signal.
- Rounding once at the end, so line items do not sum to the total.
- Not asking whether partial hours are proportional, which changes both the code and the revenue.
Key takeaways
- Ask three questions first: partial-hour policy, rounding rule, and what happens to a bad record.
- Proportional billing of a 7.5-minute allocation of four A100s at $1.10 is $0.55; rounding up to the hour would charge $4.40 for the same usage.
- Decimal for money, always; integer cents if decimal is somehow too slow, never float.
- Validate and reject loudly: unknown types and reversed intervals raise rather than returning zero.
- Test the partial hour, the month boundary, the malformed record, the free-credit floor, and that line items sum to the total.
