Documents
Atomic Registry Mutation Pattern
Atomic Registry Mutation Pattern
Type
Topic
Status
Published
Created
Mar 20, 2026
Updated
Mar 20, 2026
Created by
Dosu Bot
Updated by
Dosu Bot

Atomic Registry Mutation Pattern#

Lead Section#

The Atomic Registry Mutation Pattern (also called "validate-before-mutate") is a defensive programming pattern for preventing partial state corruption in registry-style data structures. The pattern was discovered during the implementation of FormatRegistry in the opnDossier project, specifically in PR #434. The pattern ensures that registration operations either complete entirely or fail immediately without any state changes, preventing the registry from being left in an inconsistent state when validation failures cause panics.

The pattern is particularly important for data structures that map multiple keys to single values (such as canonical names plus aliases) and use init-time or startup registration with panic-on-error semantics, following Go's database/sql driver registration pattern. By validating all conditions—including duplicate checks, nil guards, and conflict detection—before performing any map mutations, the pattern guarantees atomic registration semantics: either all map entries are written successfully, or none are written at all.

This pattern has been adopted as a project-wide standard for all registry-style data structures in opnDossier and is documented as the canonical implementation approach in AGENTS.md §5.9b.


Background and Discovery#

The FormatRegistry Partial State Problem#

The atomic registry mutation pattern was discovered during the development of FormatRegistry in PR #434, which aimed to centralize format dispatch across the opnDossier codebase. The FormatRegistry maintains two separate maps:

type FormatRegistry struct {
    mu sync.RWMutex
    handlers map[string]FormatHandler // canonical name → handler
    aliases map[string]string // alias → canonical name
}

The Register() method must write to both maps: one entry in handlers for the canonical format name, plus one entry in aliases for each alias returned by handler.Aliases(). The partial state problem arises when a panic occurs after writing the canonical handler but before completing all alias writes—for example, if the second alias conflicts with an existing registration. This leaves the registry in an inconsistent state with some keys written and others missing.

Database/SQL Driver Pattern Influence#

The pattern follows Go's database/sql driver registration approach, which panics on duplicate registration rather than returning errors. This is appropriate because registration occurs at initialization time (via init() functions or package-level var declarations), where invalid registrations represent programmer errors that should fail fast. As documented in PR #434:

Register() panics: Follows database/sql driver pattern — init-time panics on duplicate names/aliases/nil handlers.


Pattern Description#

Core Principle#

The atomic registry mutation pattern separates registration into two distinct phases:

  1. Validation Phase: Perform all checks before touching any map. If any check fails, panic immediately.
  2. Commit Phase: After all validations pass, perform all map mutations atomically (in Go's sense—all writes happen or none do).

The "atomic" guarantee is achieved by ensuring that all panics occur in Phase 1. If Phase 2 is reached, it completes without error because no further validation logic runs during mutation.

Validation Phase#

The validation phase performs comprehensive checks before any state changes:

  1. Nil handler check: Verify the handler is not nil
  2. Empty name check: Ensure the canonical format name is not empty after normalization
  3. Canonical duplicate check: Verify the canonical name doesn't already exist in r.handlers
  4. Canonical-alias conflict check: Ensure the canonical name doesn't conflict with existing aliases in r.aliases
  5. For each alias:
    • Empty alias check: Verify the alias is not empty after normalization
    • Alias duplicate check: Ensure the alias doesn't already exist in r.aliases
    • Alias-canonical conflict check: Verify the alias doesn't conflict with existing canonical names in r.handlers

During this phase, the implementation builds a local aliasKeys slice containing all validated aliases:

// Validate all aliases before mutating any state so a panic never leaves
// the registry partially registered.
aliasKeys := make([]string, 0, len(handler.Aliases()))
for _, alias := range handler.Aliases() {
    aliasKey := strings.TrimSpace(strings.ToLower(alias))
    if aliasKey == "" {
        panic(fmt.Sprintf("converter: empty alias for format %q", key))
    }
    if _, exists := r.aliases[aliasKey]; exists {
        panic(fmt.Sprintf("converter: alias %q already registered", aliasKey))
    }
    if _, exists := r.handlers[aliasKey]; exists {
        panic(fmt.Sprintf("converter: alias %q conflicts with canonical format", aliasKey))
    }

    aliasKeys = append(aliasKeys, aliasKey)
}

The comment at lines 69-70 explicitly documents the pattern's intent.

Commit Phase#

After all validation passes, the commit phase performs atomic map mutations:

// Commit: all validation passed, now mutate maps atomically.
r.handlers[key] = handler
for _, aliasKey := range aliasKeys {
    r.aliases[aliasKey] = key
}

Because all validation completed successfully in Phase 1, Phase 2 cannot panic or fail. The pre-built aliasKeys slice ensures no second call to handler.Aliases() is needed, eliminating any possibility of inconsistency between validation and commit.


Implementation Example#

The complete FormatRegistry.Register() implementation demonstrates the pattern:

func (r *FormatRegistry) Register(format string, handler FormatHandler) {
    if handler == nil {
        panic(fmt.Sprintf("converter: nil handler for format %q", format))
    }

    r.mu.Lock()
    defer r.mu.Unlock()

    key := strings.TrimSpace(strings.ToLower(format))
    if key == "" {
        panic("converter: empty format name")
    }

    // Phase 1: Validate canonical name
    if _, exists := r.handlers[key]; exists {
        panic(fmt.Sprintf("converter: format %q already registered", key))
    }
    if _, exists := r.aliases[key]; exists {
        panic(fmt.Sprintf("converter: format %q conflicts with existing alias", key))
    }

    // Phase 1: Validate all aliases
    aliasKeys := make([]string, 0, len(handler.Aliases()))
    for _, alias := range handler.Aliases() {
        aliasKey := strings.TrimSpace(strings.ToLower(alias))
        if aliasKey == "" {
            panic(fmt.Sprintf("converter: empty alias for format %q", key))
        }
        if _, exists := r.aliases[aliasKey]; exists {
            panic(fmt.Sprintf("converter: alias %q already registered", aliasKey))
        }
        if _, exists := r.handlers[aliasKey]; exists {
            panic(fmt.Sprintf("converter: alias %q conflicts with canonical format", aliasKey))
        }

        aliasKeys = append(aliasKeys, aliasKey)
    }

    // Phase 2: Commit all mutations atomically
    r.handlers[key] = handler
    for _, aliasKey := range aliasKeys {
        r.aliases[aliasKey] = key
    }
}

Benefits#

Prevents Partial State Corruption#

The primary benefit is preventing the registry from being left in an inconsistent state. Without the pattern, a panic during registration could leave some map entries written and others missing. This creates subtle bugs where some lookups succeed while related lookups fail:

  • A canonical name lookup might succeed while its alias fails
  • An alias lookup might succeed while the canonical name is missing
  • Different aliases for the same handler might resolve inconsistently

These bugs are difficult to detect at init time because they only manifest during actual usage, and the symptoms may be intermittent depending on which keys the application attempts to access.

Fail-Fast Validation#

By front-loading all validation before any writes, the pattern ensures that invalid registrations fail immediately with clear panic messages identifying the specific problem:

  • "converter: nil handler for format %q"
  • "converter: format %q already registered"
  • "converter: alias %q conflicts with canonical format"

This fail-fast behavior makes configuration errors immediately visible during startup or testing, rather than hiding them until runtime.

Thread Safety#

The pattern works in conjunction with sync.RWMutex to provide thread-safe registration. The lock is held for the entire validate-commit sequence:

r.mu.Lock()
defer r.mu.Unlock()

This ensures concurrent registration attempts are serialized, preventing race conditions during validation and commit phases.

Simplified Error Handling#

Because registration occurs at init time and panics on failure, callers don't need error handling logic. The registration either succeeds silently or panics with a descriptive message:

func newDefaultRegistry() *FormatRegistry {
    r := NewFormatRegistry()
    r.Register("markdown", &markdownHandler{}) // no error to handle
    r.Register("json", &jsonHandler{})
    r.Register("yaml", &yamlHandler{})
    return r
}

Applicability#

When to Use This Pattern#

The atomic registry mutation pattern is appropriate for data structures that exhibit all of the following characteristics:

  1. Multiple keys per value: Maps multiple lookup keys (canonical names + aliases, or multiple index keys) to single values
  2. Init-time registration: Registration occurs during program initialization via init() functions or package-level variables
  3. Panic-on-error semantics: Invalid registrations represent programmer errors that should fail fast, following the database/sql driver pattern
  4. Mutates multiple maps: Registration must write to multiple map data structures or write multiple entries to the same map

Registry-Style Implementations in opnDossier#

The pattern has been adopted across multiple registry implementations in the opnDossier codebase:

RegistryFileStatusNotes
FormatRegistryinternal/converter/registry.go✅ ImplementedReference implementation; panics on duplicate
PluginRegistryinternal/audit/plugin.go✅ ImplementedReturns errors instead of panicking
DeviceParserRegistrypkg/parser/registry.go✅ ImplementedFollows panic-on-duplicate pattern (v2.0.0)

The PluginRegistry uses a variant of the pattern that returns errors instead of panicking, suitable for runtime plugin loading scenarios:

func (r *PluginRegistry) RegisterPlugin(plugin compliance.Plugin) error {
    // Validate configuration first
    if err := plugin.ValidateConfiguration(); err != nil {
        return fmt.Errorf("invalid plugin configuration: %w", err)
    }

    r.mutex.Lock()
    defer r.mutex.Unlock()

    // Check for duplicates
    if _, exists := r.plugins[plugin.Name()]; exists {
        return fmt.Errorf("plugin %q already registered", plugin.Name())
    }

    // Commit after validation
    r.plugins[plugin.Name()] = plugin
    return nil
}

When Not to Use This Pattern#

The pattern is not appropriate for:

  • Runtime registration with error recovery: When registration can occur during normal operation and errors should be handled gracefully (use error returns like PluginRegistry)
  • Single-key mappings: When each registration writes only one map entry (no partial state is possible)
  • Transactional databases: When using actual database transactions or other true ACID-compliant systems
  • Caching/memoization: For runtime value caching where overwrites are expected (like internal/sanitizer/mapper.go)

Comparison with PluginRegistry Pattern#

The opnDossier codebase demonstrates two distinct approaches to registry mutation:

FormatRegistry: Panic-on-Duplicate Pattern#

  • Uses panics for all validation failures
  • Appropriate for init-time registration where invalid configuration is a programmer error
  • Follows database/sql driver pattern
  • Registration in init() or package-level var declarations
  • No error handling required at call sites

PluginRegistry: Error-Return Pattern#

  • Returns errors for validation failures
  • Appropriate for runtime registration where failures should be handled gracefully
  • Allows recovery from registration failures
  • Registration during application lifecycle (e.g., loading plugins from disk)
  • Requires error handling at call sites

Both patterns implement validate-before-mutate semantics, but differ in their error signaling strategy. The choice between them depends on registration timing and whether failures represent programmer errors (panic) or operational conditions (error).


Test Coverage#

The pattern's correctness is validated through comprehensive test coverage. The FormatRegistry has 76 test cases with 100% coverage, including:

  • Panic scenarios: Tests for each validation failure condition (nil handler, duplicates, conflicts)
  • Positive cases: Successful registration and retrieval of canonical names and aliases
  • Edge cases: Empty strings, whitespace normalization, case-insensitive matching
  • Concurrent access: Thread-safe read/write patterns using sync.RWMutex

Example test structure:

func TestRegister_PanicOnDuplicateAlias(t *testing.T) {
    r := NewFormatRegistry()
    r.Register("format1", &mockHandler{aliases: []string{"alias"}})

    // Should panic when second format tries to use same alias
    defer func() {
        if r := recover(); r == nil {
            t.Error("expected panic on duplicate alias")
        }
    }()

    r.Register("format2", &mockHandler{aliases: []string{"alias"}})
}

Integration with opnDossier Architecture#

DefaultRegistry Singleton#

The FormatRegistry is exposed via a package-level singleton:

var DefaultRegistry = newDefaultRegistry()

func newDefaultRegistry() *FormatRegistry {
    r := NewFormatRegistry()
    r.Register("markdown", &markdownHandler{})
    r.Register("json", &jsonHandler{})
    r.Register("yaml", &yamlHandler{})
    r.Register("text", &textHandler{})
    r.Register("html", &htmlHandler{})
    return r
}

Because registration completes at package initialization, the DefaultRegistry is read-only after startup, eliminating the need for sync primitives during normal operation.

Usage Throughout Codebase#

The DefaultRegistry serves as the single source of truth for format handling:

  • CLI validation: Options.Format.Validate() delegates to DefaultRegistry.Get()
  • Alias resolution: processor.Transform() uses DefaultRegistry.Canonical() to resolve mdmarkdown, ymlyaml
  • Shell completion: cmd.ValidFormats() derives format lists from DefaultRegistry.ValidFormats()
  • File extensions: Handler's FileExtension() method accessed via registry

Registry-Based Dispatch#

The atomic registry mutation pattern is a component of the broader Registry-Based Dispatch Pattern used throughout opnDossier. This architectural pattern replaces hardcoded switch statements with dynamic registry lookups, enabling extensibility and reducing code duplication.

Init-Time Self-Registration#

The DeviceParserRegistry (implemented in PR #437) adopts an init-time self-registration pattern for external parsers:

// external/my-device-parser/parser.go
func init() {
    parser.Register("mydevice", NewMyDeviceParser)
}

This enables compile-time plugin linking where external parsers register themselves automatically when imported, following the database/sql driver pattern.

Canonical Reference Documentation#

The pattern is documented as the canonical implementation approach in:


Relevant Code Files#

FileDescription
internal/converter/registry.goReference implementation of atomic registry mutation pattern
internal/converter/registry_test.go76 test cases with 100% coverage
internal/audit/plugin.goError-return variant for runtime plugin registration
pkg/parser/registry.goDeviceParserRegistry implementation following panic-on-duplicate pattern
docs/solutions/architecture-issues/format-dispatch-centralization-registry-pattern.mdArchitecture documentation for registry pattern

See Also#

Atomic Registry Mutation Pattern | Dosu