Billing Code Is Where Frameworks Go to Die

In 2021 I joined a project to move an aging application onto a new front end. The plan had a nice clean shape: rebuild the UI in the new stack, keep the API, delete the old code, go home. The UI migration went fine. The API migration went fine. Then somebody asked what we were going to do about the `billing/` folder, and the project got a second act.

The folder had last been touched in 2014. It predated the second front end and the rewrite before that one. Its git history showed four frameworks’ worth of idioms layered on top of each other like sediment: jQuery callbacks, Backbone models, an Angular service, and finally a handful of plain functions that everyone was afraid to open. Proration logic lived in a file called `helpers2.js`. There were seven places that computed tax.

We kept all of it. The new system wrapped it in a thin adapter and left the insides alone, because the code had been running since 2014 and had not charged a customer the wrong amount since 2016. That is a track record. Nobody rewrites a track record.

What billing actually is

bookkeeping, accounting, taxes, settlement, calculator, money, business, finance, billing, documents, office, desk, secretariat, macbook, binder, folders, bookkeeping, bookkeeping, bookkeeping, accounting, accounting, accounting, accounting, accounting, taxes, calculator, money, money, money, finance, finance, finance, finance, billing
Photo by Firmbee on Pixabay

Billing looks like CRUD from the outside: plans, subscriptions, invoices, rows in a database. From the inside it is closer to accounting. Money is an external truth, and external truths do not care about your architecture.

The core problem is exactly-once effects over an at-least-once world. Card networks retry. Webhooks get delivered twice, arrive out of order, or arrive four days late. Your job is to make sure that one real-world charge produces one row in your ledger, no matter how many times the notification knocks on the door. That means idempotency keys on every write that touches money, an event log you can replay, and state transitions that reject nonsense instead of absorbing it.

The second problem is time. Subscriptions renew at midnight, but midnight in which timezone, and what happens when a renewal and a cancellation land in the same second? We once spent three days on a bug where a customer who canceled during the renewal window got charged and refunded and charged again across two months. The code was all correct in isolation. The ordering was wrong, and only money bugs punish ordering this way.

The third problem is rounding. Ten percent off a price that is itself a discounted proration of a plan that is billed annually in euros is a place where floating-point arithmetic quietly invents fractions of a cent. Multiply a hundred thousand customers by an invented fraction of a cent and you get a spreadsheet that does not close.

Why the frameworks pass it by

Free stock photo of donner pass, lake, lake donner
Photo by Stephen Leonardi on Pexels

Rewrites follow the code that is fun: the views, the interactions, the parts with screenshots in the demo. Billing has no demo. It has edge cases that only appear at scale, at month boundaries, and in countries you do not live in. So it survives in place, and with every rewrite it picks up another layer of wrappers.

Modern payment infrastructure has absorbed a great deal of this, which is genuinely good news. Hosted checkout, subscription state machines, retry and dunning logic, tax calculation, and monthly reporting can all come from your provider instead of your team. That is the single biggest quality-of-life improvement I have watched this industry ship. Even then, the reconciliation step stays yours. Once a month somebody has to prove that the provider’s report, your database, and your bank account tell the same story, and that job falls to code that almost nobody wants to write.

What I want in a billing module now

bookkeeping, accounting, taxes, settlement, calculator, money, business, finance, billing, documents, office, desk, secretariat, macbook, binder, folders, bookkeeping, bookkeeping, bookkeeping, accounting, accounting, accounting, accounting, accounting, taxes, calculator, money, money, money, finance, finance, finance, finance, billing
Photo by Firmbee on Pixabay

After the 2021 project I started keeping a short list.

One state machine, declared in one place, with every legal transition named. Subscription states are a small set: trialing, active, past due, canceled, and friends. If the transitions live in one file that a new hire can read in twenty minutes, half the bug class disappears.

Idempotency keys on every outbound call that moves money, and a durable record of what each key did. When the provider retries a request I sent on Tuesday, I need Friday’s handler to answer “already done” instead of doing it again.

An append-only event log next to the current-state tables. When a customer writes in angry about an invoice, I want to replay the events and see exactly which decision produced which line item.

Tests with frozen clocks and fake money endpoints. Time-dependent code without an injectable clock is untestable, and untested billing code is a loaded gun.

The broader rule

The pattern generalizes past billing. Any module whose correctness is judged outside your system deserves the conservative treatment: money, email delivery, identity and permissions, scheduled jobs that run at 3am. These are the engine rooms. They get boring code, exhaustive tests, and a bias toward boring technology that has been running for years.

The `billing/` folder from that project is still running, by the way. The front end around it has been rewritten once since 2021. Somewhere in there, `helpers2.js` is quietly calculating proration for the fourth generation of engineers, and it has never been the reason anyone lost a night of sleep. That is what winning looks like in this corner of the industry. The framework on top will be gone in a few years. The ledger has to be right forever.

Dawn Rose

Back to top