Keyed Services

Keyed services allow you to register multiple implementations of the same interface, distinguishing them with unique keys. This is essential when you need different implementations for different scenarios.

When to Use Keyed Services

Use keyed services when you have:

  • Multiple database connections (primary/replica)

  • Different cache implementations (Redis/Memcached)

  • Environment-specific services (dev/staging/prod)

  • Feature flags requiring different implementations

  • Multiple payment gateways or notification channels

Basic Registration

Register services with the Name option:

// Register multiple cache implementations
services.AddSingleton(
    func() Cache { return NewRedisCache("redis://primary:6379") },
    godi.Name("primary"),
)

services.AddSingleton(
    func() Cache { return NewRedisCache("redis://cache:6379") },
    godi.Name("cache"),
)

services.AddSingleton(
    func() Cache { return NewMemoryCache() },
    godi.Name("memory"),
)

Resolving Keyed Services

Use ResolveKeyed to get a specific implementation:

// Resolve specific cache
primaryCache, err := godi.ResolveKeyed[Cache](provider, "primary")
if err != nil {
    log.Fatal("Failed to resolve primary cache:", err)
}

// Use in a service
func (s *DataService) GetUser(ctx context.Context, id string) (*User, error) {
    // Try primary cache first
    cache, _ := godi.ResolveKeyed[Cache](s.provider, "primary")
    if user, found := cache.Get(id); found {
        return user.(*User), nil
    }

    // Fallback to database
    // ...
}

Database Example

Common pattern for read replicas:

// Database connections
services.AddSingleton(
    func(config *Config) (Database, error) {
        return NewPostgresDB(config.PrimaryDB)
    },
    godi.Name("primary"),
)

services.AddSingleton(
    func(config *Config) (Database, error) {
        return NewPostgresDB(config.ReadReplicaDB)
    },
    godi.Name("replica"),
)

// Repository using different databases
type UserRepository struct {
    primaryDB Database
    replicaDB Database
}

func NewUserRepository(provider godi.ServiceProvider) (*UserRepository, error) {
    primary, err := godi.ResolveKeyed[Database](provider, "primary")
    if err != nil {
        return nil, err
    }

    replica, err := godi.ResolveKeyed[Database](provider, "replica")
    if err != nil {
        return nil, err
    }

    return &UserRepository{
        primaryDB: primary,
        replicaDB: replica,
    }, nil
}

func (r *UserRepository) Create(user *User) error {
    // Writes go to primary
    return r.primaryDB.Insert(user)
}

func (r *UserRepository) GetByID(id string) (*User, error) {
    // Reads go to replica
    return r.replicaDB.FindOne(id)
}

Notification Channels

Multiple notification services:

// Email service implementations
services.AddSingleton(
    func(config *Config) EmailService {
        return NewSMTPService(config.SMTP)
    },
    godi.Name("smtp"),
)

services.AddSingleton(
    func(config *Config) EmailService {
        return NewSendGridService(config.SendGridAPIKey)
    },
    godi.Name("sendgrid"),
)

services.AddSingleton(
    func() EmailService {
        return NewMockEmailService()
    },
    godi.Name("mock"),
)

// Notification service that chooses based on config
type NotificationService struct {
    provider godi.ServiceProvider
    config   *Config
}

func (s *NotificationService) SendEmail(to, subject, body string) error {
    // Choose service based on configuration
    serviceName := s.config.EmailProvider // "smtp", "sendgrid", or "mock"

    emailService, err := godi.ResolveKeyed[EmailService](s.provider, serviceName)
    if err != nil {
        return fmt.Errorf("email provider %s not found: %w", serviceName, err)
    }

    return emailService.Send(to, subject, body)
}

Payment Gateways

Handle multiple payment providers:

// Payment gateway implementations
services.AddSingleton(
    func(config *Config) PaymentGateway {
        return NewStripeGateway(config.StripeKey)
    },
    godi.Name("stripe"),
)

services.AddSingleton(
    func(config *Config) PaymentGateway {
        return NewPayPalGateway(config.PayPalClientID, config.PayPalSecret)
    },
    godi.Name("paypal"),
)

services.AddSingleton(
    func(config *Config) PaymentGateway {
        return NewSquareGateway(config.SquareAccessToken)
    },
    godi.Name("square"),
)

// Payment processor that routes to correct gateway
type PaymentProcessor struct {
    provider godi.ServiceProvider
}

func (p *PaymentProcessor) ProcessPayment(
    amount float64,
    currency string,
    gateway string,
) (*PaymentResult, error) {
    paymentGateway, err := godi.ResolveKeyed[PaymentGateway](p.provider, gateway)
    if err != nil {
        return nil, fmt.Errorf("payment gateway %s not available", gateway)
    }

    return paymentGateway.Charge(amount, currency)
}

Environment-Based Services

Different implementations per environment:

func ConfigureServices(services godi.ServiceCollection, env string) {
    switch env {
    case "production":
        services.AddSingleton(
            func() Logger { return NewCloudLogger() },
            godi.Name("logger"),
        )
        services.AddSingleton(
            func() Metrics { return NewDatadogMetrics() },
            godi.Name("metrics"),
        )

    case "development":
        services.AddSingleton(
            func() Logger { return NewConsoleLogger() },
            godi.Name("logger"),
        )
        services.AddSingleton(
            func() Metrics { return NewNoOpMetrics() },
            godi.Name("metrics"),
        )
    }
}

// Usage remains the same across environments
logger, _ := godi.ResolveKeyed[Logger](provider, "logger")
metrics, _ := godi.ResolveKeyed[Metrics](provider, "metrics")

Feature Flags

Toggle features with keyed services:

// Feature implementations
services.AddSingleton(
    func() SearchEngine { return NewElasticsearchEngine() },
    godi.Name("search-v2"),
)

services.AddSingleton(
    func() SearchEngine { return NewSQLSearchEngine() },
    godi.Name("search-v1"),
)

// Feature flag service
type FeatureService struct {
    provider godi.ServiceProvider
    flags    FeatureFlags
}

func (f *FeatureService) GetSearchEngine() (SearchEngine, error) {
    engineKey := "search-v1" // default
    if f.flags.IsEnabled("new-search") {
        engineKey = "search-v2"
    }

    return godi.ResolveKeyed[SearchEngine](f.provider, engineKey)
}

Storage Backends

Multiple storage options:

// Storage implementations
services.AddSingleton(
    func() Storage { return NewS3Storage() },
    godi.Name("s3"),
)

services.AddSingleton(
    func() Storage { return NewGCSStorage() },
    godi.Name("gcs"),
)

services.AddSingleton(
    func() Storage { return NewLocalStorage() },
    godi.Name("local"),
)

// File service with configurable storage
type FileService struct {
    storages map[string]Storage
}

func NewFileService(provider godi.ServiceProvider) (*FileService, error) {
    // Pre-resolve all storage backends
    storages := make(map[string]Storage)

    for _, name := range []string{"s3", "gcs", "local"} {
        storage, err := godi.ResolveKeyed[Storage](provider, name)
        if err == nil {
            storages[name] = storage
        }
    }

    return &FileService{storages: storages}, nil
}

func (s *FileService) SaveFile(file []byte, backend string) error {
    storage, ok := s.storages[backend]
    if !ok {
        return fmt.Errorf("storage backend %s not available", backend)
    }

    return storage.Save(file)
}

Injecting Keyed Services

Use struct tags in parameter objects:

// Parameter object with keyed dependencies
type ServiceParams struct {
    godi.In

    PrimaryDB   Database `name:"primary"`
    ReplicaDB   Database `name:"replica"`
    CacheRedis  Cache    `name:"redis"`
    CacheMemory Cache    `name:"memory"`
}

func NewComplexService(params ServiceParams) *ComplexService {
    return &ComplexService{
        primaryDB:   params.PrimaryDB,
        replicaDB:   params.ReplicaDB,
        cacheRedis:  params.CacheRedis,
        cacheMemory: params.CacheMemory,
    }
}

Dynamic Key Resolution

Resolve services with dynamic keys:

type MultiTenantService struct {
    provider godi.ServiceProvider
}

func (s *MultiTenantService) GetTenantDB(tenantID string) (Database, error) {
    // Each tenant has their own database
    dbKey := fmt.Sprintf("tenant-%s", tenantID)

    db, err := godi.ResolveKeyed[Database](s.provider, dbKey)
    if err != nil {
        // Fallback to default
        return godi.ResolveKeyed[Database](s.provider, "default")
    }

    return db, nil
}

// Register tenant databases dynamically
func RegisterTenantDatabases(services godi.ServiceCollection, tenants []Tenant) {
    for _, tenant := range tenants {
        t := tenant // capture loop variable
        services.AddSingleton(
            func() Database {
                return NewPostgresDB(t.DatabaseURL)
            },
            godi.Name(fmt.Sprintf("tenant-%s", t.ID)),
        )
    }
}

Testing with Keyed Services

Easy to mock specific implementations:

func TestPaymentProcessing(t *testing.T) {
    services := godi.NewServiceCollection()

    // Register mock gateways
    services.AddSingleton(
        func() PaymentGateway {
            return &MockGateway{
                chargeFunc: func(amount float64, currency string) (*PaymentResult, error) {
                    return &PaymentResult{Success: true}, nil
                },
            }
        },
        godi.Name("stripe"),
    )

    services.AddSingleton(
        func() PaymentGateway {
            return &MockGateway{
                chargeFunc: func(amount float64, currency string) (*PaymentResult, error) {
                    return nil, errors.New("paypal unavailable")
                },
            }
        },
        godi.Name("paypal"),
    )

    provider, _ := services.BuildServiceProvider()
    defer provider.Close()

    processor := &PaymentProcessor{provider: provider}

    // Test successful payment
    result, err := processor.ProcessPayment(100, "USD", "stripe")
    assert.NoError(t, err)
    assert.True(t, result.Success)

    // Test failed payment
    _, err = processor.ProcessPayment(100, "USD", "paypal")
    assert.Error(t, err)
}

Best Practices

1. Use Constants for Keys

const (
    DBPrimary  = "primary"
    DBReplica  = "replica"
    DBAnalytics = "analytics"
)

services.AddSingleton(NewPrimaryDB, godi.Name(DBPrimary))

2. Document Available Keys

// CacheService provides access to different cache implementations.
// Available keys:
// - "redis": Redis-based cache (production)
// - "memory": In-memory cache (development)
// - "distributed": Hazelcast distributed cache
type CacheService interface {
    Get(key string, cacheType string) (interface{}, error)
}

3. Provide Fallbacks

func ResolveWithFallback[T any](provider godi.ServiceProvider, keys ...string) (T, error) {
    var zero T

    for _, key := range keys {
        service, err := godi.ResolveKeyed[T](provider, key)
        if err == nil {
            return service, nil
        }
    }

    return zero, fmt.Errorf("no service found for keys: %v", keys)
}

// Usage
cache, err := ResolveWithFallback[Cache](provider, "redis", "memory", "noop")

4. Validate at Startup

func ValidateKeyedServices(provider godi.ServiceProvider) error {
    requiredKeys := map[string]reflect.Type{
        "primary": reflect.TypeOf((*Database)(nil)).Elem(),
        "replica": reflect.TypeOf((*Database)(nil)).Elem(),
        "redis":   reflect.TypeOf((*Cache)(nil)).Elem(),
    }

    for key, serviceType := range requiredKeys {
        if !provider.IsKeyedService(serviceType, key) {
            return fmt.Errorf("required keyed service not found: %s[%s]",
                serviceType, key)
        }
    }

    return nil
}

Summary

Keyed services provide flexibility for:

  • Multiple implementations of the same interface

  • Environment-specific configurations

  • Feature toggling

  • Multi-tenancy

  • A/B testing

They maintain type safety while allowing runtime selection of implementations, making your application more flexible and testable.