Keyed Services
Register and resolve multiple implementations of the same interface using keys.
Understanding Keyed Services
Keyed services allow you to register multiple implementations of the same type and resolve them by a unique key:
// Register multiple cache implementations
services.AddSingleton(NewRedisCache, godi.Name("redis"))
services.AddSingleton(NewMemoryCache, godi.Name("memory"))
services.AddSingleton(NewDiskCache, godi.Name("disk"))
// Resolve specific implementation
redisCache := godi.MustResolveKeyed[Cache](provider, "redis")
memoryCache := godi.MustResolveKeyed[Cache](provider, "memory")
Registration with Keys
Named Services
Use godi.Name() to assign a key to a service:
// Database connections
func NewPrimaryDB(config *Config) Database {
return connectDB(config.PrimaryHost)
}
func NewReplicaDB(config *Config) Database {
return connectDB(config.ReplicaHost)
}
func NewAnalyticsDB(config *Config) Database {
return connectDB(config.AnalyticsHost)
}
// Register with names
services.AddSingleton(NewPrimaryDB, godi.Name("primary"))
services.AddSingleton(NewReplicaDB, godi.Name("replica"))
services.AddSingleton(NewAnalyticsDB, godi.Name("analytics"))
Resolution
// Resolve by key
primary := godi.MustResolveKeyed[Database](provider, "primary")
replica := godi.MustResolveKeyed[Database](provider, "replica")
// With error handling
analytics, err := godi.ResolveKeyed[Database](provider, "analytics")
if err != nil {
// Handle error
}
Common Use Cases
Multiple Database Connections
type DatabaseManager interface {
Read(query string) Result
Write(query string) Result
RunAnalytics(query string) Result
}
type databaseManager struct {
primary Database
replica Database
analytics Database
}
func NewDatabaseManager(provider godi.Provider) DatabaseManager {
return &databaseManager{
primary: godi.MustResolveKeyed[Database](provider, "primary"),
replica: godi.MustResolveKeyed[Database](provider, "replica"),
analytics: godi.MustResolveKeyed[Database](provider, "analytics"),
}
}
func (m *databaseManager) Read(query string) Result {
// Use replica for reads
return m.replica.Query(query)
}
func (m *databaseManager) Write(query string) Result {
// Use primary for writes
return m.primary.Exec(query)
}
func (m *databaseManager) RunAnalytics(query string) Result {
// Use analytics DB for heavy queries
return m.analytics.Query(query)
}
Strategy Pattern
// Payment strategies
type PaymentStrategy interface {
ProcessPayment(amount float64) (*Transaction, error)
ValidateCard(card *Card) error
}
// Implementations
type StripeStrategy struct { client *stripe.Client }
type PayPalStrategy struct { client *paypal.Client }
type SquareStrategy struct { client *square.Client }
type PaymentStrategyKey string
const (
StripeStrategyKey PaymentStrategyKey = "stripe"
PayPalStrategyKey PaymentStrategyKey = "paypal"
SquareStrategyKey PaymentStrategyKey = "square"
)
// Register strategies
services.AddSingleton(NewStripeStrategy, godi.Name(StripeStrategyKey), godi.As[PaymentStrategy]())
services.AddSingleton(NewPayPalStrategy, godi.Name(PayPalStrategyKey), godi.As[PaymentStrategy]())
services.AddSingleton(NewSquareStrategy, godi.Name(SquareStrategyKey), godi.As[PaymentStrategy]())
// Payment service selects strategy
type PaymentService struct {
provider godi.Provider
}
func (s *PaymentService) ProcessPayment(method StripeStrategyKey, amount float64) (*Transaction, error) {
strategy := godi.MustResolveKeyed[PaymentStrategy](s.provider, string(method))
return strategy.ProcessPayment(amount)
}
Environment-Specific Services
// Different implementations per environment
func RegisterServices(services godi.Collection, env string) {
switch env {
case "development":
services.AddSingleton(NewMockEmailSender, godi.Name("email"), godi.As[EmailSender]())
case "staging":
services.AddSingleton(NewSandboxEmailSender, godi.Name("email"), godi.As[EmailSender]())
case "production":
services.AddSingleton(NewSESEmailSender, godi.Name("email"), godi.As[EmailSender]())
}
}
// Always resolve the same way
emailSender := godi.MustResolveKeyed[EmailSender](provider, "email")
Advanced Patterns
Dynamic Key Resolution
type CacheManager struct {
provider godi.Provider
config *Config
}
func (m *CacheManager) GetCache() Cache {
// Dynamically determine which cache to use
cacheType := m.config.GetCacheType() // Returns "redis", "memory", etc.
return godi.MustResolveKeyed[Cache](m.provider, cacheType)
}
Fallback Pattern
func GetCacheWithFallback(provider godi.Provider) Cache {
// Try primary cache
cache, err := godi.ResolveKeyed[Cache](provider, "redis")
if err == nil {
return cache
}
// Fallback to memory cache
cache, err = godi.ResolveKeyed[Cache](provider, "memory")
if err == nil {
return cache
}
// Last resort - create default
return NewDefaultCache()
}
Factory with Keys
type RepositoryFactory struct {
provider godi.Provider
}
func (f *RepositoryFactory) GetRepository(entity string) Repository {
// Map entity types to repository keys
keyMap := map[string]string{
"user": "userRepo",
"order": "orderRepo",
"product": "productRepo",
}
key, ok := keyMap[entity]
if !ok {
return NewGenericRepository()
}
return godi.MustResolveKeyed[Repository](f.provider, key)
}
With Parameter Objects
Using Named Dependencies in Parameter Objects
type ServiceParams struct {
godi.In
PrimaryDB Database `name:"primary"`
ReplicaDB Database `name:"replica"`
RedisCache Cache `name:"redis"`
MemoryCache Cache `name:"memory"`
}
func NewComplexService(params ServiceParams) ComplexService {
return &complexService{
primary: params.PrimaryDB,
replica: params.ReplicaDB,
redis: params.RedisCache,
memory: params.MemoryCache,
}
}
// godi automatically resolves the named dependencies
services.AddScoped(NewComplexService)
Multi-Tenant Applications
Tenant-Specific Services
// Register tenant-specific databases
for _, tenant := range tenants {
services.AddSingleton(
func(t Tenant) func() Database {
return func() Database {
return NewTenantDatabase(t.ConnectionString)
}
}(tenant),
godi.Name(tenant.ID),
)
}
// Resolve for specific tenant
func GetTenantDatabase(provider godi.Provider, tenantID string) Database {
return godi.MustResolveKeyed[Database](provider, tenantID)
}
Request-Scoped Tenant Resolution
type TenantService struct {
provider godi.Provider
}
func (s *TenantService) GetDatabase(ctx context.Context) Database {
// Extract tenant from context
tenantID := GetTenantID(ctx)
// Resolve tenant-specific database
return godi.MustResolveKeyed[Database](s.provider, tenantID)
}
Testing with Keyed Services
Mock Specific Implementations
func TestPaymentService(t *testing.T) {
services := godi.NewCollection()
// Register mock for specific payment method
services.AddSingleton(NewMockStripeStrategy, godi.Name("stripe"), godi.As[PaymentStrategy]())
provider, _ := services.Build()
paymentService := NewPaymentService(provider)
// Test stripe payment
tx, err := paymentService.ProcessPayment("stripe", 100.00)
assert.NoError(t, err)
assert.NotNil(t, tx)
}
Override Specific Keys
func SetupTestProvider(overrides map[string]any) godi.Provider {
services := godi.NewCollection()
// Register defaults
services.AddSingleton(NewDefaultLogger, godi.Name("logger"))
services.AddSingleton(NewDefaultCache, godi.Name("cache"))
// Override specific keys for testing
for key, service := range overrides {
services.AddSingleton(service, godi.Name(key))
}
provider, err := services.Build()
if err != nil {
panic(err)
}
return provider
}
// Usage in test
provider := SetupTestProvider(map[string]any{
"cache": NewMockCache(), // Override cache with mock
})
Best Practices
Use descriptive keys - Make keys self-documenting
Document available keys - List all possible keys in comments
Handle missing keys gracefully - Provide fallbacks when appropriate
Validate keys at build time - Fail fast for invalid configurations
Keep keys consistent - Use constants for repeated keys
Key Constants
// Define constants for keys
const (
PrimaryDB = "primary"
ReplicaDB = "replica"
RedisCache = "redis"
MemoryCache = "memory"
)
// Use constants in registration
services.AddSingleton(NewPrimaryDB, godi.Name(PrimaryDB))
services.AddSingleton(NewRedisCache, godi.Name(RedisCache))
// Use constants in resolution
primary := godi.MustResolveKeyed[Database](provider, PrimaryDB)
cache := godi.MustResolveKeyed[Cache](provider, RedisCache)
Common Pitfalls
Key Conflicts
// ❌ Don't register same key twice
services.AddSingleton(NewServiceA, godi.Name("service"))
services.AddSingleton(NewServiceB, godi.Name("service")) // Error!
// ✅ Use unique keys
services.AddSingleton(NewServiceA, godi.Name("serviceA"))
services.AddSingleton(NewServiceB, godi.Name("serviceB"))
Type Mismatches
// ❌ Wrong type assertion
services.AddSingleton(NewLogger, godi.Name("logger"))
cache := godi.MustResolveKeyed[Cache](provider, "logger") // Panic!
// ✅ Correct type
logger := godi.MustResolveKeyed[Logger](provider, "logger")
Next Steps
Explore Service Groups
Learn about Parameter Objects
Understand Result Objects