Herald
1.x
Docs/Herald/Memory Store
Open

Reading5 min
Updated31 Jul 2026
Sourcev1/stores/memory.mdx

The memory store (herald/store/memory) is a fully in-memory implementation of store.Store. It keeps all data in Go maps protected by a sync.RWMutex, making it safe for concurrent access. No external dependencies, database connections, or migrations are required.

Installation01

The memory store is included in the Herald module. No additional packages are needed.

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

Creating a Store02

store := memory.New()

memory.New() returns a *memory.Store with all internal maps initialized and ready to use.

Constructor03

func New() *Store

No arguments are needed. The store initializes maps for all 7 entity types:

  • providers -- map[string]*provider.Provider

  • templates -- map[string]*template.Template

  • versions -- map[string]*template.Version

  • messages -- map[string]*message.Message

  • notifications -- map[string]*inbox.Notification

  • preferences -- map[string]*preference.Preference (key: "appID:userID")

  • scopedConfigs -- map[string]*scope.Config (key: "appID:scope:scopeID")

Implemented Interfaces04

The memory store satisfies all six subsystem store interfaces plus the three lifecycle methods that form the composite store.Store interface:

InterfacePackageMethods
provider.Storegithub.com/xraph/herald/providerCreateProvider, GetProvider, UpdateProvider, DeleteProvider, ListProviders, ListAllProviders
template.Storegithub.com/xraph/herald/templateCreateTemplate, GetTemplate, GetTemplateBySlug, UpdateTemplate, DeleteTemplate, ListTemplates, ListTemplatesByChannel, CreateVersion, GetVersion, UpdateVersion, DeleteVersion, ListVersions
message.Storegithub.com/xraph/herald/messageCreateMessage, GetMessage, UpdateMessageStatus, ListMessages
inbox.Storegithub.com/xraph/herald/inboxCreateNotification, GetNotification, DeleteNotification, MarkRead, MarkAllRead, UnreadCount, ListNotifications
preference.Storegithub.com/xraph/herald/preferenceGetPreference, SetPreference, DeletePreference
scope.Storegithub.com/xraph/herald/scopeGetScopedConfig, SetScopedConfig, DeleteScopedConfig, ListScopedConfigs

Compile-time interface check:

var _ store.Store = (*Store)(nil)

Concurrency Safety05

All methods acquire either a read lock (RLock) or a write lock (Lock) on the internal sync.RWMutex. This makes the store safe for concurrent use from multiple goroutines, including parallel test execution.

Lifecycle Methods06

MethodBehaviour
Migrate(ctx)No-op -- returns nil
Ping(ctx)Always returns nil
Close()No-op -- returns nil

The memory store has no schema to create and no connection to check, so all lifecycle methods succeed immediately. Calling Migrate is good practice for consistency with other backends.

Key Scheme07

Entities are keyed using their TypeID string representation. Composite-keyed entities use colon-separated keys:

EntityKey format
Providers, Templates, Versions, Messages, NotificationsTypeID string (e.g., hpvd_...)
Preferences"appID:userID"
Scoped Configs"appID:scope:scopeID"

Template Version Association08

When retrieving templates, the memory store automatically attaches associated versions by scanning the versions map for matching TemplateID values. Deleting a template also removes all its associated versions.

No Migration Needed09

Since data is stored in Go maps, there is no schema to create or migrate. The Migrate method is a no-op that always returns nil.

Complete Example10

package main

import (
    "context"
    "fmt"
    "log"
    "time"

    "github.com/xraph/herald"
    "github.com/xraph/herald/driver/email"
    "github.com/xraph/herald/id"
    "github.com/xraph/herald/provider"
    "github.com/xraph/herald/store/memory"
    "github.com/xraph/herald/template"
)

func main() {
    ctx := context.Background()

    // Create the in-memory store.
    store := memory.New()

    // Create a Herald instance with the memory store.
    h, err := herald.New(
        herald.WithStore(store),
        herald.WithDriver(&email.SMTPDriver{}),
    )
    if err != nil {
        log.Fatal(err)
    }

    // Create a provider.
    now := time.Now().UTC()
    p := &provider.Provider{
        ID:      id.NewProviderID(),
        AppID:   "myapp",
        Name:    "SMTP",
        Channel: "email",
        Driver:  "smtp",
        Credentials: map[string]string{
            "host": "smtp.example.com",
            "port": "587",
        },
        Enabled:   true,
        CreatedAt: now,
        UpdatedAt: now,
    }
    if err := store.CreateProvider(ctx, p); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Provider created: %s\n", p.ID)

    // Create a template.
    tmpl := &template.Template{
        ID:        id.NewTemplateID(),
        AppID:     "myapp",
        Slug:      "welcome",
        Name:      "Welcome Email",
        Channel:   "email",
        Category:  "transactional",
        Enabled:   true,
        CreatedAt: now,
        UpdatedAt: now,
    }
    if err := store.CreateTemplate(ctx, tmpl); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Template created: %s\n", tmpl.ID)

    _ = h
}

Swapping to a Persistent Backend11

The memory store implements the same store.Store interface as all other backends. Switching to a production store is a single-line change:

// Development / testing
store := memory.New()

// Production (PostgreSQL)
store := postgres.New(db)

// Production (MongoDB)
store := mongo.New(db)

When to Use12

  • Unit tests -- no setup, no teardown, zero dependencies.

  • Development -- quick iteration without running a database server.

  • CI pipelines -- fast, deterministic tests with no external services.

  • Prototyping -- explore Herald's API surface without infrastructure.

When Not to Use13

  • Production -- data is lost on process restart.

  • Multi-instance deployments -- each process has its own isolated store.

  • Performance benchmarking -- behaviour characteristics differ from a real database.