Ledger

Memory Store

In-memory store implementation for testing and development.

The Memory Store is Ledger's in-memory storage implementation. It stores all data in Go maps protected by a sync.RWMutex, making it thread-safe but non-persistent. It is designed for unit tests, integration tests, and local development.

Setup

import "github.com/xraph/ledger/store/memory"

memStore := memory.New()

engine := ledger.New(memStore,
    ledger.WithMeterConfig(1, 0), // Immediate flush for testing
    ledger.WithEntitlementCacheTTL(0), // No caching for testing
)

Internal structure

The memory store maintains separate maps for each entity type:

type Store struct {
    mu sync.RWMutex

    plans            map[string]*plan.Plan
    subscriptions    map[string]*subscription.Subscription
    usageEvents      []meter.UsageEvent
    entitlementCache map[string]*entitlement.Result
    cacheExpiry      map[string]time.Time
    invoices         map[string]*invoice.Invoice
    coupons          map[string]*coupon.Coupon
}

All operations acquire the appropriate lock (read or write) before accessing data.

Implements store.Store

The memory store implements the full store.Store interface including:

  • Plan methodsCreatePlan, GetPlan, GetPlanBySlug, ListPlans, UpdatePlan, DeletePlan, ArchivePlan
  • Subscription methodsCreateSubscription, GetSubscription, GetActiveSubscription, ListSubscriptions, UpdateSubscription, CancelSubscription
  • Meter methodsIngestBatch, Aggregate, AggregateMulti, QueryUsage, PurgeUsage
  • Entitlement methodsGetCached, SetCached, Invalidate, InvalidateFeature
  • Invoice methodsCreateInvoice, GetInvoice, ListInvoices, UpdateInvoice, GetInvoiceByPeriod, ListPendingInvoices, MarkInvoicePaid, MarkInvoiceVoided
  • Coupon methodsCreateCoupon, GetCoupon, GetCouponByID, ListCoupons, UpdateCoupon, DeleteCoupon
  • Core methodsMigrate (no-op), Ping (always succeeds), Close (no-op)

Use cases

Use caseRecommended?
Unit testsYes
Integration testsYes
Local developmentYes
CI pipelinesYes
ProductionNo
Multi-process environmentsNo

Limitations

  • No persistence — Data is lost when the process exits.
  • Single process only — Cannot be shared across multiple instances.
  • No real caching — Entitlement cache uses the same process memory, so cache-miss behavior cannot be tested accurately.
  • No SQL queries — Cannot test SQL-specific behavior like indexes, constraints, or migrations.

Testing patterns

Basic test setup

func TestBillingFlow(t *testing.T) {
    store := memory.New()
    engine := ledger.New(store)

    ctx := context.Background()
    ctx = context.WithValue(ctx, "tenant_id", "test-tenant")
    ctx = context.WithValue(ctx, "app_id", "test-app")

    if err := engine.Start(ctx); err != nil {
        t.Fatal(err)
    }
    defer engine.Stop()

    // Create plan
    p := &plan.Plan{
        ID:   id.NewPlanID(),
        Name: "test-plan",
        Features: []plan.Feature{
            {Key: "api-calls", Type: plan.FeatureMetered, Limit: 100},
        },
    }
    if err := engine.CreatePlan(ctx, p); err != nil {
        t.Fatal(err)
    }

    // Test entitlements, metering, invoicing...
}

Test with plugins

func TestPluginHooks(t *testing.T) {
    store := memory.New()
    recorder := &mockRecorder{}

    engine := ledger.New(store,
        ledger.WithPlugin(audithook.New(recorder)),
    )

    // Actions will trigger audit events through the plugin
}

Comparison with PostgreSQL store

FeatureMemoryPostgreSQL
PersistenceNoneFull
Multi-processNoYes
MigrationsNo-opFull schema migrations
PerformanceVery fast (no I/O)Network I/O
Setup requiredNoneDatabase connection
Multi-tenancyMap-based filteringRow-level security

On this page