Keyed Services
Keyed services let you register multiple implementations of the same interface and choose which one to use.
When to Use Keyed Services
Use keyed services when you have:
Multiple database connections (primary, replica)
Different implementations for different scenarios
Feature flags or environment-specific services
Multiple external API clients
Basic Example
// Define interface
type Logger interface {
Log(message string)
}
// Multiple implementations
type FileLogger struct {
filename string
}
func NewFileLogger() Logger {
return &FileLogger{filename: "app.log"}
}
type ConsoleLogger struct{}
func NewConsoleLogger() Logger {
return &ConsoleLogger{}
}
// Register with keys
var LoggingModule = godi.NewModule("logging",
godi.AddSingleton(NewFileLogger, godi.Name("file")),
godi.AddSingleton(NewConsoleLogger, godi.Name("console")),
godi.AddSingleton(NewCloudLogger, godi.Name("cloud")),
)
// Use specific implementation
func main() {
services := godi.NewServiceCollection()
services.AddModules(LoggingModule)
provider, _ := services.BuildServiceProvider()
defer provider.Close()
// Get specific logger
fileLogger, _ := godi.ResolveKeyed[Logger](provider, "file")
fileLogger.Log("Writing to file")
consoleLogger, _ := godi.ResolveKeyed[Logger](provider, "console")
consoleLogger.Log("Writing to console")
}
Real-World Example: Multiple Databases
// Database interface
type Database interface {
Query(sql string) ([]Row, error)
Execute(sql string) error
}
// Different database connections
func NewPrimaryDB(config *Config) Database {
return &PostgresDB{
connString: config.PrimaryDB,
maxConns: 100,
}
}
func NewReplicaDB(config *Config) Database {
return &PostgresDB{
connString: config.ReplicaDB,
maxConns: 50,
readOnly: true,
}
}
func NewAnalyticsDB(config *Config) Database {
return &ClickhouseDB{
connString: config.AnalyticsDB,
}
}
// Module with keyed databases
var DatabaseModule = godi.NewModule("database",
godi.AddSingleton(NewConfig),
godi.AddSingleton(NewPrimaryDB, godi.Name("primary")),
godi.AddSingleton(NewReplicaDB, godi.Name("replica")),
godi.AddSingleton(NewAnalyticsDB, godi.Name("analytics")),
)
// Repository using specific database
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) CreateUser(user *User) error {
// Write to primary
return r.primaryDB.Execute("INSERT INTO users...")
}
func (r *UserRepository) GetUser(id string) (*User, error) {
// Read from replica
rows, err := r.replicaDB.Query("SELECT * FROM users WHERE id = ?")
// ...
}
Using Parameter Objects
For cleaner code, use parameter objects with named dependencies:
type RepositoryDeps struct {
godi.In
Primary Database `name:"primary"`
Replica Database `name:"replica"`
Analytics Database `name:"analytics" optional:"true"`
Logger Logger
}
func NewUserRepository(deps RepositoryDeps) *UserRepository {
repo := &UserRepository{
primary: deps.Primary,
replica: deps.Replica,
logger: deps.Logger,
}
// Analytics is optional
if deps.Analytics != nil {
repo.analytics = deps.Analytics
}
return repo
}
Environment-Based Selection
Choose implementations based on environment:
var CacheModule = godi.NewModule("cache",
godi.AddSingleton(func() Cache {
switch os.Getenv("CACHE_TYPE") {
case "redis":
return NewRedisCache()
case "memcached":
return NewMemcachedCache()
default:
return NewMemoryCache()
}
}),
)
// Or use keyed services
var CacheModule = godi.NewModule("cache",
godi.AddSingleton(NewRedisCache, godi.Name("redis")),
godi.AddSingleton(NewMemcachedCache, godi.Name("memcached")),
godi.AddSingleton(NewMemoryCache, godi.Name("memory")),
// Default cache
godi.AddSingleton(func(provider godi.ServiceProvider) Cache {
cacheType := os.Getenv("CACHE_TYPE")
if cacheType == "" {
cacheType = "memory"
}
cache, err := godi.ResolveKeyed[Cache](provider, cacheType)
if err != nil {
// Fallback to memory
cache, _ = godi.ResolveKeyed[Cache](provider, "memory")
}
return cache
}),
)
Feature Flags Pattern
Use keyed services for feature toggles:
// Payment processors
var PaymentModule = godi.NewModule("payment",
godi.AddSingleton(NewStripeProcessor, godi.Name("stripe")),
godi.AddSingleton(NewPayPalProcessor, godi.Name("paypal")),
godi.AddSingleton(NewBraintreeProcessor, godi.Name("braintree")),
// Feature flag based selection
godi.AddScoped(func(provider godi.ServiceProvider, config *Config) PaymentProcessor {
// Check feature flags
if config.Features.UseNewPaymentProvider {
processor, _ := godi.ResolveKeyed[PaymentProcessor](provider, "braintree")
return processor
}
// Default
processor, _ := godi.ResolveKeyed[PaymentProcessor](provider, "stripe")
return processor
}),
)
Testing with Keyed Services
Easy to mock specific implementations:
var TestPaymentModule = godi.NewModule("test-payment",
godi.AddSingleton(func() PaymentProcessor {
return &MockPaymentProcessor{
shouldSucceed: true,
}
}, godi.Name("stripe")),
godi.AddSingleton(func() PaymentProcessor {
return &MockPaymentProcessor{
shouldSucceed: false, // Test failures
}
}, godi.Name("failing")),
)
func TestPaymentFlow(t *testing.T) {
services := godi.NewServiceCollection()
services.AddModules(TestPaymentModule)
provider, _ := services.BuildServiceProvider()
// Test success case
successProcessor, _ := godi.ResolveKeyed[PaymentProcessor](provider, "stripe")
assert.NoError(t, successProcessor.Process(payment))
// Test failure case
failProcessor, _ := godi.ResolveKeyed[PaymentProcessor](provider, "failing")
assert.Error(t, failProcessor.Process(payment))
}
Best Practices
1. Use Clear, Descriptive Keys
// ✅ Good keys
godi.Name("primary-db")
godi.Name("replica-db")
godi.Name("analytics-db")
// ❌ Vague keys
godi.Name("db1")
godi.Name("db2")
godi.Name("db3")
2. Document Available Keys
// Package database provides database connections.
//
// Available keys:
// - "primary": Read-write connection to primary database
// - "replica": Read-only connection to replica
// - "analytics": Connection to analytics database (ClickHouse)
var DatabaseModule = godi.NewModule("database",
// ...
)
3. Provide Defaults When Possible
var NotificationModule = godi.NewModule("notification",
// Keyed implementations
godi.AddSingleton(NewEmailNotifier, godi.Name("email")),
godi.AddSingleton(NewSMSNotifier, godi.Name("sms")),
godi.AddSingleton(NewPushNotifier, godi.Name("push")),
// Default notifier (not keyed)
godi.AddSingleton(func() Notifier {
return NewEmailNotifier() // Email as default
}),
)
4. Consider Using Enums for Keys
type CacheType string
const (
CacheTypeRedis CacheType = "redis"
CacheTypeMemcached CacheType = "memcached"
CacheTypeMemory CacheType = "memory"
)
// Use enum values as keys
godi.AddSingleton(NewRedisCache, godi.Name(string(CacheTypeRedis)))
// Resolve with type safety
cache, _ := godi.ResolveKeyed[Cache](provider, string(CacheTypeRedis))
When NOT to Use Keyed Services
Don’t use keyed services for:
Simple feature toggles (use configuration instead)
Services that should be composed (use decorators)
When you need ALL implementations (use service groups)
Summary
Keyed services are perfect for:
Multiple implementations of the same interface
Environment-specific services
Feature flags and A/B testing
Multiple external service connections
Use descriptive keys and provide good documentation for available options!