Ask a multi-tenant Rails app where its architecture actually hurts and it will point at money before it points at tenant scoping or the configuration layer. We’ve run the payments module for FastTravel since day zero, through its growth to every major airport in Norway, and three years in it’s still the part that gets continuous engineering. It was built well; billing simply sits where tenants differ most and mistakes are least forgivable, in the one subsystem where the outside world’s rules outrank your design preferences.
If you’re sketching a multi-tenant product, this is the subsystem to design with your most paranoid engineer. Here’s what it looks like past the tutorial horizon.
Why billing diverges first
Tenants share your product and negotiate your money. The product features they use are mostly common; the commercial terms almost never are. One tenant pays per transaction, the next wants a monthly invoice with consolidated line items, the third has a fee schedule someone agreed to in a meeting two years ago. At FastTravel, every airport came with its own fee structures and invoicing rules on top of shared booking flows. That asymmetry is structural: companies buy software as a product and pay for it as a negotiation.
So the first design consequence: fee schedules are tenant configuration data, owned and versioned like the rest of the configuration layer, and they have no business living as constants in code. The day a salesperson agrees to a custom rate, your architecture either absorbs it as a data row or absorbs it as a deploy. You want to have chosen in advance.
The parts the tutorials end before
Subscription tutorials cover the happy 80%: a card, a plan, a webhook. Production billing lives in the other 20%, and three of its regulars are worth meeting before they meet you.
Corrections are not edits. When an invoice is wrong, you don’t change it; you issue a correcting document that references it, because accountants and tax authorities require the paper trail, and in much of Europe that’s law rather than preference. This single fact reshapes the data model: documents become append-only, every amount has a history, and “just fix the row” becomes the bug. Corrections handling was part of FastTravel’s payments scope from early on, and it’s the feature whose absence you discover at the worst possible moment, in front of someone’s auditor.
B2B money moves by invoice. Card-and-webhook thinking assumes consumer payments. Businesses want consolidated invoices, payment terms, references to their PO numbers, and they pay by bank transfer days or weeks later. That means your billing system tracks owed-versus-paid as a first-class state over time, with sequential document numbering that your app may have promised the world long before tenancy arrived.
Usage arrives from the physical world. The charge that tests your architecture is the one no user clicked. At FastTravel, automatic passage fees get billed when vehicles pass airport infrastructure: barriers, chips, cameras. Events arrive from hardware, sometimes late, sometimes duplicated, and each must end up on exactly one invoice at the rate of the right airport. Your version may be API calls or sensor data instead of barriers, but the shape recurs everywhere: an event stream on one side, accounting documents on the other, and the billing engine as the careful translation between them.
Build, buy, or the honest middle
Nearly everything ranking for this question is written by a billing vendor. We sell nothing in this category (we do build custom software for clients, so weigh that bias too), and this is the view from that seat.
Buy when your commercial model is standard: per-seat or tiered subscriptions, card payments, self-serve customers. Stripe Billing and its competitors have spent more engineering on proration edge cases than you ever will, and rebuilding that is vanity.
The build case appears exactly where the tutorials ended: negotiated per-tenant terms, B2B invoicing with legal numbering and corrections, usage events from the physical world. Generic subscription vendors handle these awkwardly, and even the usage-billing specialists struggle with negotiated terms and jurisdiction-specific correcting documents, because those generalize poorly. FastTravel built its billing in-house because no off-the-shelf product spoke “per-airport fee schedules applied to barrier events, invoiced with corrections.”
The honest middle is common: a vendor for card processing and dunning, your own layer for fee logic, document generation, and the event-to-invoice translation. The boundary to draw is between moving money (commodity, buy it) and deciding what’s owed (your business, own it). Taxes, VAT, multi-currency, and revenue recognition each deserve their own treatment; budget for them in the same owned layer, and note that late events arriving after an invoice is finalized are exactly what correcting documents exist for.
Design rules that survived three years
A few principles have paid for themselves repeatedly on the money paths. Treat billing as append-only: documents reference documents, nothing financial gets mutated, every number is reconstructible. Make idempotency a habit, because payment events arrive twice and the second arrival must cost zero. Put your most honest tests here (the suite must mean something most where errors cost most), and test per-tenant divergence explicitly: the rate that applies at one tenant and not another is precisely the bug a single-tenant test suite can’t see.
And staff it like it matters. The same engineer has owned FastTravel’s payments from day zero, three years and counting, and that continuity is itself an architectural decision. Billing accumulates context the way the codebase accumulates documents: who negotiated what, and which fee rule applies where. A multi-tenant product can survive rotating generalists almost everywhere. The money paths reward an owner.