Billing Domain
The billing domain provides payments, subscriptions, and invoicing with a vendor-independent architecture. The domain layer uses integer-cents arithmetic via the Money value object and has zero knowledge of payment providers like Stripe or Paddle.
Install
Section titled “Install”npx @backcap/cli add billingDomain Model
Section titled “Domain Model”Money Value Object
Section titled “Money Value Object”All monetary amounts use integer cents to avoid floating-point issues.
import { Money } from "./domains/billing/domain/value-objects/money.vo";
const price = Money.create(2999, "USD"); // $29.99const tax = Money.create(450, "USD"); // $4.50
const total = price.unwrap().add(tax.unwrap()); // $34.49const discounted = total.unwrap().multiply(0.9); // 10% offCustomer Entity
Section titled “Customer Entity”import { Customer } from "./domains/billing/domain/entities/customer.entity";
const result = Customer.create({ id: crypto.randomUUID(), email: "user@example.com", name: "Jane Doe",});Subscription Entity
Section titled “Subscription Entity”Subscriptions compose SubscriptionStatus, Money, and BillingPeriod value objects. Mutations are immutable — cancel() and changePlan() return new instances.
import { Subscription } from "./domains/billing/domain/entities/subscription.entity";
const sub = Subscription.create({ id: crypto.randomUUID(), customerId: "cust-1", planId: "plan-pro", status: "active", priceAmount: 2999, priceCurrency: "USD", billingInterval: "monthly", billingStartDate: new Date(), billingEndDate: nextMonth,});
// Immutable cancelconst canceled = sub.unwrap().cancel("user request");Invoice Entity
Section titled “Invoice Entity”import { Invoice } from "./domains/billing/domain/entities/invoice.entity";
const invoice = Invoice.create({ id: crypto.randomUUID(), customerId: "cust-1", subscriptionId: "sub-1", amountValue: 2999, amountCurrency: "USD", status: "open", dueDate: new Date("2026-04-01"),});
const paid = invoice.unwrap().markPaid();Use Cases
Section titled “Use Cases”Subscription Management
Section titled “Subscription Management”| Use Case | Description |
|---|---|
CreateSubscription | Creates subscription via payment provider, persists to repository |
CancelSubscription | Cancels subscription both locally and with provider |
ChangeSubscriptionPlan | Updates plan and price on active subscription |
GetSubscription | Retrieves subscription by ID |
Payment Processing
Section titled “Payment Processing”| Use Case | Description |
|---|---|
ProcessPayment | Charges customer via payment provider |
RefundPayment | Refunds full or partial amount |
GetPaymentHistory | Lists customer’s invoices |
Invoice Management
Section titled “Invoice Management”| Use Case | Description |
|---|---|
GenerateInvoice | Creates and persists a new invoice |
GetInvoice | Retrieves invoice by ID |
ListInvoices | Lists all invoices for a customer |
The billing domain defines four ports for adapter injection:
IPaymentProvider—createCustomer,charge,refund,createSubscription,cancelSubscription,attachPaymentMethodICustomerRepository—findById,findByEmail,saveISubscriptionRepository—findById,findByCustomerId,saveIInvoiceRepository—findById,findByCustomerId,save
Contract & Factory
Section titled “Contract & Factory”Wire everything through the contract factory:
import { createBillingService } from "./domains/billing/contracts";
const billing = createBillingService({ customerRepository: prismaCustomerRepo, subscriptionRepository: prismaSubscriptionRepo, invoiceRepository: prismaInvoiceRepo, paymentProvider: stripeAdapter, // or any IPaymentProvider implementation});
const result = await billing.createSubscription({ customerId: "cust-1", planId: "plan-pro", priceAmount: 2999, priceCurrency: "USD", billingInterval: "monthly",});Domain Events
Section titled “Domain Events”| Event | Payload |
|---|---|
SubscriptionCreated | subscriptionId, customerId, planId |
SubscriptionCanceled | subscriptionId, customerId, reason |
PaymentSucceeded | customerId, amount, currency |
PaymentFailed | customerId, amount, currency, reason |
InvoiceGenerated | invoiceId, customerId, amount, currency |
Swapping Providers
Section titled “Swapping Providers”To use a different payment provider (e.g. Paddle, Braintree), implement IPaymentProvider with that provider’s SDK. No changes to domain or application layers are needed — just inject your implementation through the factory.