Ledger

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:

  1. Plan definition — Create plans with features (metered, licensed, or binary), pricing tiers, and billing intervals.

  2. Subscription creation — Subscribe customers to plans with trials, discounts, and custom pricing overrides.

  3. Entitlement checking — Before allowing feature access, check entitlements with sub-millisecond latency using the cache layer.

  4. Usage metering — Track usage events in a non-blocking, high-throughput buffer that batches and persists to the store.

  5. Invoice generation — At billing period end, aggregate usage, apply pricing tiers, calculate taxes, and generate detailed invoices.

  6. 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 operations
  • webhook.Plugin — HTTP webhooks for billing events
  • audit.Plugin — Audit trail for compliance
  • tax.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())     // 5399

TypeID 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  // Event

Benefits:

  • 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

PackageImport pathPurpose
ledgergithub.com/xraph/ledgerMain engine and configuration
plan.../planPlan and feature definitions
subscription.../subscriptionSubscription lifecycle management
meter.../meterUsage event tracking and batching
entitlement.../entitlementFeature access checking with cache
invoice.../invoiceInvoice generation and line items
payment.../paymentPayment processing and methods
customer.../customerCustomer profiles and metadata
coupon.../couponDiscounts and promotional codes
tax.../taxTax calculation and rates
types.../typesMoney, TypeID, and common types
store.../storeStorage interface
store/postgres.../store/postgresPostgreSQL implementation
store/sqlite.../store/sqliteSQLite implementation
store/redis.../store/redisRedis cache layer
store/memory.../store/memoryIn-memory for testing
provider.../providerPayment provider interface
provider/stripe.../provider/stripeStripe integration
provider/paddle.../provider/paddlePaddle integration
plugin.../pluginPlugin system and registry
webhook.../webhookWebhook delivery system

On this page