zoobzio January 24, 2026 Edit this page

Architecture

This document explains how slush implements type-safe service storage and retrieval. It's intended for contributors and users who want to understand the internals.

Component Overview

┌─────────────────────────────────────────────────────────────┐
│                        Public API                           │
│  Register[T]()  Use[T]()  Services()  Reset()  Unregister[] │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                        Registry                             │
│            map[reflect.Type]service (interface)             │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                      handle[T] (generic)                    │
│    impl T │ guards []Guard │ interfaceFQDN │ implFQDN       │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                    service (interface)                      │
│         fqdns() │ guards() │ guardCount()                   │
└─────────────────────────────────────────────────────────────┘

Type Erasure Pattern

The core challenge: Go maps require a uniform value type, but we want to store handle[Database], handle[Mailer], etc. in the same map.

Solution: Interface-based type erasure

// Non-generic interface for storage
type service interface {
    fqdns() (iface, impl string)
    guards() []Guard
    guardCount() int
}

// Generic implementation
type handle[T any] struct {
    impl          T
    g             []Guard
    interfaceFQDN string
    implFQDN      string
}

// handle[T] satisfies service
func (h *handle[T]) fqdns() (string, string) { ... }
func (h *handle[T]) guards() []Guard         { ... }
func (h *handle[T]) guardCount() int         { ... }

// Registry stores the interface
var services = map[reflect.Type]service

On registration, we create a *handle[T] and store it as service.

On retrieval, we type-assert back to *handle[T]:

func Use[T any](ctx context.Context) (T, error) {
    svc := services[reflect.TypeFor[T]()]
    h := svc.(*handle[T])  // Safe: keyed by same type
    return h.impl, nil
}

This preserves type safety—impl is stored as T, not any.

Registration Flow

Register[Database](db)
        │
        ▼
┌───────────────────────────────────┐
│ 1. Compute FQDNs                  │
│    - interfaceFQDN from TypeFor[T]│
│    - implFQDN from TypeOf(impl)   │
└───────────────────────────────────┘
        │
        ▼
┌───────────────────────────────────┐
│ 2. Create handle[T]               │
│    - Store impl T                 │
│    - Initialize empty guards      │
└───────────────────────────────────┘
        │
        ▼
┌───────────────────────────────────┐
│ 3. Store in registry              │
│    - Lock mutex                   │
│    - services[key] = handle       │
│    - Unlock mutex                 │
└───────────────────────────────────┘
        │
        ▼
┌───────────────────────────────────┐
│ 4. Emit capitan event             │
│    - SignalRegistered             │
│    - interface + impl FQDNs       │
└───────────────────────────────────┘
        │
        ▼
┌───────────────────────────────────┐
│ 5. Return Handle[T]               │
│    - Wraps *handle[T]             │
│    - Exposes .Guard() method      │
└───────────────────────────────────┘

Retrieval Flow

Use[Database](ctx)
        │
        ▼
┌───────────────────────────────────┐
│ 1. Lookup in registry             │
│    - RLock mutex                  │
│    - Get services[TypeFor[T]()]   │
│    - RUnlock mutex                │
└───────────────────────────────────┘
        │
        ▼
┌───────────────────────────────────┐
│ 2. Check existence                │
│    - If not found: emit event,    │
│      return ErrNotFound           │
└───────────────────────────────────┘
        │
        ▼
┌───────────────────────────────────┐
│ 3. Type-assert to *handle[T]      │
│    - Safe because key matches     │
└───────────────────────────────────┘
        │
        ▼
┌───────────────────────────────────┐
│ 4. Run guards                     │
│    - For each guard in order:     │
│      - Call guard(ctx)            │
│      - If error: emit event,      │
│        return ErrAccessDenied     │
└───────────────────────────────────┘
        │
        ▼
┌───────────────────────────────────┐
│ 5. Emit success event             │
│    - SignalAccessed               │
└───────────────────────────────────┘
        │
        ▼
┌───────────────────────────────────┐
│ 6. Return h.impl                  │
│    - Type T, no casting needed    │
└───────────────────────────────────┘

Design Q&A

Why a global registry instead of explicit dependency injection?

Go generics can't have type parameters on methods. Register[T] must be a package-level function. A method like registry.Register[T]() isn't valid Go.

Why reflect.Type as the map key?

It's the only way to differentiate handle[Database] from handle[Mailer] at runtime. reflect.TypeFor[T]() is computed at compile time, so there's no runtime reflection cost for the key.

Why pre-compute FQDNs at registration?

To avoid reflection in the hot path (Use). FQDNs are computed once and stored as strings. Use only calls pre-computed string accessors.

Why the service interface?

To enable Services() enumeration without knowing T. The interface provides accessors for metadata that don't require the type parameter.

Performance Characteristics

OperationComplexityNotes
RegisterO(1)Map insert + one reflection call
Use (no guards)O(1)Map lookup + type assertion
Use (with guards)O(n)n = number of guards
ServicesO(n)n = number of registered services
ResetO(1)Map reallocation

Concurrency: All operations are protected by sync.RWMutex. Multiple Use calls can run concurrently; Register and Reset acquire exclusive locks.

Next Steps