Every SaaS billing integration starts the same way: you pick a provider, pull in their package, wire it up — and three months later when the business wants to add a second gateway (or swap to a Malaysian one like BayarCash or ToyyibPay because Stripe doesn't do local rails), you discover your entire subscription layer is welded to the first provider's package. Different webhook shapes, different status vocabularies, different model assumptions. You're not adding a gateway; you're re-architecting.

I kept watching this happen — especially in the Malaysian market, where the "obvious" global packages assume a gateway that half my clients can't actually use. So I built cleaniquecoders/laravel-billing around one inversion: the gateway is the plugin, not the package. The engine owns subscription and invoice state. A gateway is a single contract your app implements. This post is less about the API surface and more about why it's shaped this way — because the shape is the whole point.

The core decision: one package, gateways as a contract

The temptation when building a billing library is to ship laravel-billing-stripe, laravel-billing-bayarcash, laravel-billing-toyyibpay, and so on. It feels modular. It's actually a maintenance trap — every gateway sub-package re-implements the same subscription lifecycle slightly differently, and the core can never assume a stable shape because each adapter bends it.