e when your product introduces credit pools or outcome-based pricing. The goal is to select a billing layer that survives pricing evolution without requiring infrastructure replacement.
Core Solution
Building a resilient billing architecture requires abstracting the monetization layer from both the payment processor and the pricing model. The following implementation demonstrates a TypeScript-based adapter pattern that supports hybrid pricing, version-controlled configuration, and PSP agnosticism.
Step 1: Define the Billing Interface
Start by establishing a contract that isolates your application code from vendor-specific implementations. This enables swapping engines without rewriting business logic.
export interface BillingAdapter {
createSubscription(customerId: string, planId: string, options?: SubscriptionOptions): Promise<Subscription>;
recordConsumption(customerId: string, metricId: string, quantity: number, metadata?: Record<string, unknown>): Promise<ConsumptionEvent>;
applyCredit(customerId: string, creditId: string, amount: number): Promise<CreditTransaction>;
generateInvoice(customerId: string, period: BillingPeriod): Promise<Invoice>;
getEntitlements(customerId: string): Promise<Entitlement[]>;
}
export interface SubscriptionOptions {
trialDays?: number;
billingCycle: 'monthly' | 'annual';
currency: string;
}
export interface BillingPeriod {
start: Date;
end: Date;
}
Step 2: Implement a Declarative Pricing Catalog
Hardcoding pricing logic in application code creates maintenance debt. Instead, use a declarative catalog that maps products, meters, and entitlements to a structured configuration. This configuration can be versioned, tested, and deployed via CI/CD.
type PricingRule =
| { type: 'flat'; amount: number; currency: string }
| { type: 'tiered'; tiers: { upTo: number; rate: number }[]; currency: string }
| { type: 'credit'; poolId: string; deductionRate: number };
export interface ProductCatalog {
productId: string;
name: string;
billingModel: 'subscription' | 'usage' | 'hybrid';
rules: PricingRule[];
entitlements: string[];
}
export const catalog: ProductCatalog[] = [
{
productId: 'pro-tier',
name: 'Professional',
billingModel: 'hybrid',
rules: [
{ type: 'flat', amount: 299, currency: 'USD' },
{ type: 'tiered', tiers: [{ upTo: 10000, rate: 0.005 }, { upTo: Infinity, rate: 0.003 }], currency: 'USD' },
{ type: 'credit', poolId: 'ai-credits', deductionRate: 1 }
],
entitlements: ['api-access', 'ai-inference', 'priority-support']
}
];
Step 3: Build the Adapter Factory
The factory pattern routes requests to the appropriate vendor implementation based on environment configuration. This keeps PSP selection decoupled from business logic.
import { StripeBillingAdapter } from './adapters/stripe';
import { GenericBillingAdapter } from './adapters/generic';
export class BillingFactory {
static create(provider: 'stripe' | 'generic', config: ProviderConfig): BillingAdapter {
switch (provider) {
case 'stripe':
return new StripeBillingAdapter(config.apiKey);
case 'generic':
return new GenericBillingAdapter(config.endpoint, config.authToken);
default:
throw new Error(`Unsupported billing provider: ${provider}`);
}
}
}
Architecture Decisions & Rationale
- Interface Segregation: The
BillingAdapter contract prevents vendor lock-in. If your pricing model shifts or you need multi-PSP routing for regional compliance, you implement a new adapter without touching core application code.
- Declarative Catalog: Pricing rules are data, not logic. This enables automated testing, rollback capabilities, and audit trails. Finance and engineering can collaborate on the same configuration file.
- PSP Agnosticism: Routing through a factory allows you to switch processors or run parallel providers for redundancy. This is critical for global SaaS companies navigating regional payment regulations.
- Credit & Metering Abstraction: By treating credits and usage as first-class primitives in the interface, the architecture supports hybrid models natively. No orchestration layer is required to stitch subscription and consumption data.
Pitfall Guide
1. The Current-State Trap
Explanation: Selecting a billing platform based on today's pricing model instead of the 12-24 month roadmap. Subscription engines struggle when usage or credit pools are introduced later.
Fix: Model your pricing evolution before vendor selection. If hybrid pricing is on the roadmap, prioritize platforms with native multi-surface support or build an abstraction layer that can route to hybrid engines.
2. Dashboard-Only Configuration
Explanation: Relying on vendor UIs for pricing changes breaks CI/CD pipelines, introduces manual error risk, and prevents version control. Finance teams become bottlenecks for product launches.
Fix: Adopt billing-as-code. Store pricing catalogs in version control, run validation tests in CI, and deploy changes through infrastructure pipelines. Use CLI or API endpoints for configuration updates.
3. Payment Processor Lock-In
Explanation: Tying billing logic directly to a single PSP's SDK or webhook format. When regional compliance requires alternative processors or when negotiating better rates, migration becomes a full rewrite.
Fix: Abstract payment routing behind a provider interface. Maintain a mapping layer that translates internal billing events to PSP-specific payloads. Test multi-PSP routing in staging before production rollout.
4. Underestimating Multi-Entity Complexity
Explanation: Building billing for a single legal entity and currency. Global expansion triggers VAT/GST compliance, local payment methods, and consolidated reporting requirements that single-entity architectures cannot handle.
Fix: Design the ledger with entity and currency dimensions from day one. Use multi-entity invoicing primitives and ensure your billing platform supports jurisdictional tax routing or integrates with a dedicated tax engine.
5. Over-Engineering Before Product-Market Fit
Explanation: Building custom metering pipelines and hybrid pricing engines when the product is still validating its monetization strategy. Engineering resources are consumed by billing infrastructure instead of core product development.
Fix: Start with bundled solutions (e.g., ecosystem-native billing or MoR wrappers) during early stages. Migrate to a custom abstraction layer or hybrid-native platform only when pricing complexity or scale demands it.
6. Misaligning GTM Motion with Billing Capabilities
Explanation: PLG products require instant self-serve provisioning and automated dunning. Sales-led products require CPQ, contract management, and custom discounting. Using a platform optimized for the wrong motion creates operational friction.
Fix: Map your sales motion to billing requirements. PLG teams should prioritize automated checkout, trial conversion, and usage metering. Enterprise teams should prioritize contract lifecycle, quote-to-cash workflows, and revenue recognition compliance.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| PLG startup with simple recurring plans | Ecosystem-bundled engine (e.g., Stripe Billing) | Fastest time-to-market, tight SDK integration, minimal overhead | 0.5% to 0.8% of billing volume |
| Scaling SaaS introducing usage/credits | Hybrid-native platform (e.g., Solvimon) | Native multi-surface ledger, headless configuration, PSP agnosticism | Free tier up to $1M billed, then custom |
| Global self-serve selling internationally | Merchant-of-Record wrapper (e.g., Paddle) | Handles VAT/GST/sales tax compliance, reduces legal overhead | Revenue share percentage |
| Finance-led mid-market with strict GAAP needs | Subscription engine with compliance focus (e.g., Maxio, Chargebee) | Audit-ready reporting, ARR/NRR tracking, revenue recognition built-in | Contact sales (typically mid-five to six figures annually) |
| Large enterprise with multi-product complexity | Enterprise billing platform (e.g., Zuora) | Deep compliance, customizable discounting, multi-entity support | High TCO, multi-quarter implementation |
| Developer-first API/AI consumption product | Consumption-first engine (e.g., Metronome) | Real-time event ingestion, complex rating logic, high-fidelity metering | Contact sales |
Configuration Template
// billing.config.ts
import { ProductCatalog, PricingRule } from './types';
export const pricingCatalog: ProductCatalog[] = [
{
productId: 'starter',
name: 'Starter',
billingModel: 'subscription',
rules: [
{ type: 'flat', amount: 49, currency: 'USD' }
],
entitlements: ['basic-api', 'email-support']
},
{
productId: 'growth',
name: 'Growth',
billingModel: 'hybrid',
rules: [
{ type: 'flat', amount: 199, currency: 'USD' },
{ type: 'tiered', tiers: [
{ upTo: 50000, rate: 0.004 },
{ upTo: 200000, rate: 0.0025 },
{ upTo: Infinity, rate: 0.0015 }
], currency: 'USD' },
{ type: 'credit', poolId: 'compute-credits', deductionRate: 1 }
],
entitlements: ['advanced-api', 'priority-support', 'custom-integrations']
}
];
export const billingConfig = {
provider: process.env.BILLING_PROVIDER as 'stripe' | 'generic',
currency: 'USD',
taxEngine: 'auto-calculate', // or 'manual' / 'mor'
multiEntity: process.env.MULTI_ENTITY === 'true',
catalog: pricingCatalog
};
Quick Start Guide
- Initialize the abstraction layer: Clone the billing adapter repository, install dependencies, and configure environment variables for your target PSP and tax posture.
- Deploy the pricing catalog: Add your product definitions to
billing.config.ts, run the validation script (npm run validate-catalog), and commit to version control.
- Connect the adapter: Use the factory pattern to instantiate the billing client in your application entry point. Route subscription creation and consumption recording through the interface.
- Run integration tests: Execute the test suite to verify metering accuracy, credit deduction logic, and invoice generation. Validate webhook handling for payment events and dunning retries.
- Monitor and iterate: Deploy to staging, simulate hybrid pricing scenarios, and verify revenue recognition alignment. Roll out to production with feature flags for gradual pricing surface activation.