Architecture
How Ledger's billing packages fit together.
Ledger is organized as a set of focused Go packages for usage-based billing. The ledger package provides the main engine that coordinates all billing operations. Other packages define domain models, stores, and providers.
Package diagram
┌──────────────────────────────────────────────────────────────────┐
│ ledger.Ledger │
│ CreatePlan / UpdatePlan / GetPlan / ListPlans │
│ CreateSubscription / UpdateSubscription / CancelSubscription │
│ Meter / Entitled / GenerateInvoice / CollectPayment │
│ CreateCoupon / ApplyCoupon / CreateCustomer / UpdateCustomer │
├──────────────────────────────────────────────────────────────────┤
│ Billing flow │
│ 1. Define plans with features and pricing tiers │
│ 2. Create customer subscriptions to plans │
│ 3. Check entitlements with sub-millisecond cache │
│ 4. Meter usage events with high-throughput batching │
│ 5. Generate invoices with line items and taxes │
│ 6. Process payments through providers (Stripe, Paddle, etc) │
├──────────────────────┬───────────────────────────────────────────┤
│ plugin.Registry │ meter.Buffer │
│ OnMeterEvent │ 10K+ events/second ingestion │
│ OnInvoiceGenerated │ Automatic batching and flushing │
│ OnPaymentProcessed │ Configurable batch size and interval │
│ OnSubscriptionChange │ Non-blocking API │
│ (16 lifecycle hooks) │ │
├──────────────────────┴───────────────────────────────────────────┤
│ store.Store │
│ Plans / Features / Subscriptions / Customers / Invoices │
│ Usage events / Entitlements / Coupons / Tax rates │
│ Audit trail / Webhooks / Migrations │
├──────────────────────────────────────────────────────────────────┤
│ PostgreSQL / SQLite / Redis / Memory │
│ Production-ready stores with TypeID identifiers │
│ Multi-tenancy with context-based isolation │
└──────────────────────────────────────────────────────────────────┘Engine construction
ledger.New accepts functional options:
engine := ledger.New(store,
// Metering configuration
ledger.WithMeterConfig(
100, // Batch size
5*time.Second, // Flush interval
),
// Entitlement caching
ledger.WithEntitlementCacheTTL(30*time.Second),
// Payment provider
ledger.WithProvider(stripeProvider),
// Plugins for lifecycle hooks
ledger.WithPlugin(metricsPlugin),
ledger.WithPlugin(webhookPlugin),
// Structured logging
ledger.WithLogger(slog.Default()),
)All components are interfaces — swap any with your own implementation.
Billing flow
When processing usage-based billing, Ledger follows this flow:
-
Plan definition — Create plans with features (metered, licensed, or binary), pricing tiers, and billing intervals.
-
Subscription creation — Subscribe customers to plans with trials, discounts, and custom pricing overrides.
-
Entitlement checking — Before allowing feature access, check entitlements with sub-millisecond latency using the cache layer.
-
Usage metering — Track usage events in a non-blocking, high-throughput buffer that batches and persists to the store.
-
Invoice generation — At billing period end, aggregate usage, apply pricing tiers, calculate taxes, and generate detailed invoices.
-
Payment collection — Process payments through integrated providers (Stripe, Paddle, custom) with retry logic and webhook handling.
Performance architecture
Sub-millisecond entitlements
Request → Cache Check (0.01ms) → Hit? → Return
↓ Miss
Store Query (1-2ms) → Cache Update → Return- In-memory LRU cache with configurable TTL
- Tenant-scoped keys prevent cache pollution
- Automatic invalidation on subscription changes
High-throughput metering
Meter() → Ring Buffer → Batch Full? → Flush to Store
↓ No
Timer Elapsed? → Flush to Store- Lock-free ring buffer for event collection
- Configurable batching (size and time thresholds)
- Non-blocking API returns immediately
- 10K+ events/second sustained throughput
Multi-tenancy
context.Context carries tenant and app identifiers through every layer:
ctx = context.WithValue(ctx, "tenant_id", "acme_corp")
ctx = context.WithValue(ctx, "app_id", "production")Isolation is enforced at multiple levels:
- Store layer — All queries include
WHERE tenant_id = ? - Cache layer — Keys prefixed with
tenant:{id}: - Metering buffer — Separate buffers per tenant
- Plugin hooks — Tenant context passed to all plugins
Cross-tenant access is structurally impossible.
Plugin system
Plugins implement lifecycle hooks for extensibility:
type Plugin interface {
Name() string
}
// Optional hooks (implement any subset)
type MeterPlugin interface {
OnMeterEvent(ctx context.Context, event meter.Event) error
}
type InvoicePlugin interface {
OnInvoiceGenerated(ctx context.Context, invoice Invoice) error
}
type PaymentPlugin interface {
OnPaymentProcessed(ctx context.Context, payment Payment) error
}Built-in plugins:
metrics.Plugin— Prometheus metrics for all operationswebhook.Plugin— HTTP webhooks for billing eventsaudit.Plugin— Audit trail for compliancetax.Plugin— Tax calculation integrations
Type-safe money
All currency amounts use integer cents to avoid floating-point errors:
type Money struct {
Amount int64 // Cents (or smallest currency unit)
Currency Currency // USD, EUR, GBP, etc
}
// Helper constructors
amount := types.USD(4999) // $49.99
tax := amount.Multiply(0.08) // 8% tax = $4.00
total := amount.Add(tax) // $53.99
// Formatting
fmt.Println(total.Format()) // "$53.99"
fmt.Println(total.Cents()) // 5399TypeID identifiers
All entities use type-prefixed, K-sortable identifiers:
pln_01hqx3qfz5ekth2h5y6z6r099x // Plan
sub_01hqx3qg8w5kjyhwkrp5j4wsy3 // Subscription
inv_01hqx3qgf2c8mhwy9s6zwmf3yh // Invoice
cst_01hqx3qgn9v3rywqk8zxr7t9kj // Customer
cpn_01hqx3qgw4ekqhxyz6z6r099xa // Coupon
evt_01hqx3qh4r5kjyhwkrp5j4wsyb // EventBenefits:
- Type safety — Can't pass invoice ID where plan ID expected
- K-sortable — Chronological ordering without timestamps
- URL safe — No encoding needed
- Globally unique — No collisions across systems
Package index
| Package | Import path | Purpose |
|---|---|---|
ledger | github.com/xraph/ledger | Main engine and configuration |
plan | .../plan | Plan and feature definitions |
subscription | .../subscription | Subscription lifecycle management |
meter | .../meter | Usage event tracking and batching |
entitlement | .../entitlement | Feature access checking with cache |
invoice | .../invoice | Invoice generation and line items |
payment | .../payment | Payment processing and methods |
customer | .../customer | Customer profiles and metadata |
coupon | .../coupon | Discounts and promotional codes |
tax | .../tax | Tax calculation and rates |
types | .../types | Money, TypeID, and common types |
store | .../store | Storage interface |
store/postgres | .../store/postgres | PostgreSQL implementation |
store/sqlite | .../store/sqlite | SQLite implementation |
store/redis | .../store/redis | Redis cache layer |
store/memory | .../store/memory | In-memory for testing |
provider | .../provider | Payment provider interface |
provider/stripe | .../provider/stripe | Stripe integration |
provider/paddle | .../provider/paddle | Paddle integration |
plugin | .../plugin | Plugin system and registry |
webhook | .../webhook | Webhook delivery system |