Skip to content
Holits
Platform Engineering3 min readBy Holits

Why billing runs as its own service, not a module

Isolating billing behind its own database, its own Stripe integration and an internally authenticated boundary — so a billing-provider outage degrades checkout, not the product itself.

The easy default for billing is a module inside the main API: a few routes, a Stripe client, a webhook handler, done. It's less infrastructure to run and it keeps everything in one deploy. It also means the product's core function and its billing integration now share a failure domain — a Stripe outage, a webhook processing bug, or a billing-schema migration can take down the thing customers are actually paying for, not just the part that charges them for it.

Drawing the boundary where the failure domains actually differ

We run billing as a separate service, with its own database access and its own Stripe integration, reachable only through an internally authenticated call from the core API. The browser never talks to billing directly either — every checkout and portal interaction still routes through the product's own API surface, which just happens to forward the relevant calls internally.

The reasoning is about blast radius, not elegance. VAT validation — the actual product — has nothing to do with whether Stripe's API is currently healthy. If billing goes down, checkout degrades. If the core API goes down, billing degrading with it wouldn't have helped anyone anyway. Splitting them means those two failure modes stay independent, and an incident in one doesn't read, to an on-call engineer, as an incident in the other.

What the boundary buys beyond uptime

Independent deploy cadence. Billing logic — plan changes, webhook handling, pricing experiments — changes on a different schedule than the core product. A separate service means those changes ship without a full core-API deploy, and a core-API deploy doesn't need to carry billing risk it has nothing to do with.

A narrower blast radius for billing-specific bugs. Stripe webhook processing has its own class of correctness problems — replay handling, event ordering, idempotency — that are easier to reason about, test and audit in isolation than tangled into request handlers that also serve the product's actual traffic.

A single, auditable boundary for where money-moving logic lives. When billing is its own service, "everything that touches Stripe" is one codebase, not a module scattered across the same routers that serve unrelated features. That matters operationally — it's the difference between reviewing one service's access to a Stripe secret key and auditing every route file for where that key might have leaked in.

The trade-off, honestly

A separate service is more infrastructure: another deployable, another set of health checks, another network hop between the core API and billing on every checkout-adjacent request. That cost is real and worth naming rather than waving away. It's worth paying specifically when the thing being isolated has a materially different failure profile, a materially different change cadence, and materially higher consequences for something like a leaked credential — which billing, almost everywhere, does.

The general shape

This isn't a rule specific to billing. Any subsystem whose outage shouldn't mean the product's outage, whose secrets shouldn't sit in the same blast radius as everything else, and whose change cadence doesn't match the core product's, is a candidate for the same boundary. The question worth asking before defaulting to "just a module" is whether that subsystem failing should ever be allowed to mean the product failing — and if the answer is no, the architecture should make that true, not just intend it.