Service Groups

Service groups let you collect multiple services of the same type and inject them as a slice. Perfect for plugin systems, validators, or middleware chains.

Basic Example

// Common interface
type Validator interface {
    Validate(data interface{}) error
}

// Multiple validators
type EmailValidator struct{}
func (v *EmailValidator) Validate(data interface{}) error {
    email, ok := data.(string)
    if !ok || !strings.Contains(email, "@") {
        return errors.New("invalid email")
    }
    return nil
}

type PhoneValidator struct{}
func (v *PhoneValidator) Validate(data interface{}) error {
    phone, ok := data.(string)
    if !ok || len(phone) < 10 {
        return errors.New("invalid phone")
    }
    return nil
}

// Register as group
var ValidationModule = godi.NewModule("validation",
    godi.AddSingleton(func() Validator {
        return &EmailValidator{}
    }, godi.Group("validators")),

    godi.AddSingleton(func() Validator {
        return &PhoneValidator{}
    }, godi.Group("validators")),

    godi.AddSingleton(func() Validator {
        return &AddressValidator{}
    }, godi.Group("validators")),
)

// Use all validators
type ValidationService struct {
    validators []Validator
}

func NewValidationService(params struct {
    godi.In
    Validators []Validator `group:"validators"`
}) *ValidationService {
    return &ValidationService{
        validators: params.Validators,
    }
}

func (s *ValidationService) ValidateAll(data interface{}) error {
    for _, validator := range s.validators {
        if err := validator.Validate(data); err != nil {
            return err
        }
    }
    return nil
}

Real-World Example: HTTP Middleware

// Middleware interface
type Middleware interface {
    Handle(next http.Handler) http.Handler
}

// Various middleware
type LoggingMiddleware struct {
    logger Logger
}

func NewLoggingMiddleware(logger Logger) Middleware {
    return &LoggingMiddleware{logger: logger}
}

func (m *LoggingMiddleware) Handle(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        m.logger.Info("Request started", "path", r.URL.Path)

        next.ServeHTTP(w, r)

        m.logger.Info("Request completed",
            "path", r.URL.Path,
            "duration", time.Since(start))
    })
}

type AuthMiddleware struct {
    authService AuthService
}

func NewAuthMiddleware(authService AuthService) Middleware {
    return &AuthMiddleware{authService: authService}
}

func (m *AuthMiddleware) Handle(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if !m.authService.ValidateToken(token) {
            http.Error(w, "Unauthorized", 401)
            return
        }
        next.ServeHTTP(w, r)
    })
}

// Register middleware
var MiddlewareModule = godi.NewModule("middleware",
    godi.AddScoped(NewLoggingMiddleware, godi.Group("middleware")),
    godi.AddScoped(NewAuthMiddleware, godi.Group("middleware")),
    godi.AddScoped(NewRateLimitMiddleware, godi.Group("middleware")),
    godi.AddScoped(NewCORSMiddleware, godi.Group("middleware")),
)

// HTTP server using middleware chain
type HTTPServer struct {
    middleware []Middleware
    router     *mux.Router
}

func NewHTTPServer(
    router *mux.Router,
    params struct {
        godi.In
        Middleware []Middleware `group:"middleware"`
    },
) *HTTPServer {
    return &HTTPServer{
        middleware: params.Middleware,
        router:     router,
    }
}

func (s *HTTPServer) Start() {
    // Build middleware chain
    handler := http.Handler(s.router)

    // Apply in reverse order (so first registered runs first)
    for i := len(s.middleware) - 1; i >= 0; i-- {
        handler = s.middleware[i].Handle(handler)
    }

    log.Fatal(http.ListenAndServe(":8080", handler))
}

Plugin System Example

// Plugin interface
type Plugin interface {
    Name() string
    Initialize(app *Application) error
    Shutdown() error
}

// Various plugins
type MetricsPlugin struct {
    collector *MetricsCollector
}

func NewMetricsPlugin(collector *MetricsCollector) Plugin {
    return &MetricsPlugin{collector: collector}
}

func (p *MetricsPlugin) Name() string { return "metrics" }

func (p *MetricsPlugin) Initialize(app *Application) error {
    app.Router.Handle("/metrics", p.collector.Handler())
    return nil
}

// Plugin registration
var PluginModule = godi.NewModule("plugins",
    godi.AddSingleton(NewMetricsPlugin, godi.Group("plugins")),
    godi.AddSingleton(NewHealthCheckPlugin, godi.Group("plugins")),
    godi.AddSingleton(NewAdminPlugin, godi.Group("plugins")),
)

// Application with plugins
type Application struct {
    plugins []Plugin
    Router  *mux.Router
}

func NewApplication(
    router *mux.Router,
    params struct {
        godi.In
        Plugins []Plugin `group:"plugins"`
    },
) *Application {
    return &Application{
        plugins: params.Plugins,
        Router:  router,
    }
}

func (app *Application) Start() error {
    // Initialize all plugins
    for _, plugin := range app.plugins {
        log.Printf("Initializing plugin: %s", plugin.Name())
        if err := plugin.Initialize(app); err != nil {
            return fmt.Errorf("failed to initialize %s: %w", plugin.Name(), err)
        }
    }

    return nil
}

func (app *Application) Shutdown() error {
    // Shutdown in reverse order
    for i := len(app.plugins) - 1; i >= 0; i-- {
        if err := app.plugins[i].Shutdown(); err != nil {
            log.Printf("Error shutting down %s: %v",
                app.plugins[i].Name(), err)
        }
    }
    return nil
}

Event System Example

// Event handler interface
type EventHandler interface {
    EventType() string
    Handle(event Event) error
}

// Various event handlers
type UserCreatedHandler struct {
    emailService EmailService
}

func NewUserCreatedHandler(emailService EmailService) EventHandler {
    return &UserCreatedHandler{emailService: emailService}
}

func (h *UserCreatedHandler) EventType() string { return "user.created" }

func (h *UserCreatedHandler) Handle(event Event) error {
    user := event.Data.(*User)
    return h.emailService.SendWelcomeEmail(user.Email)
}

// Register handlers
var EventModule = godi.NewModule("events",
    godi.AddScoped(NewUserCreatedHandler, godi.Group("event-handlers")),
    godi.AddScoped(NewOrderPlacedHandler, godi.Group("event-handlers")),
    godi.AddScoped(NewPaymentProcessedHandler, godi.Group("event-handlers")),
)

// Event dispatcher
type EventDispatcher struct {
    handlers map[string][]EventHandler
}

func NewEventDispatcher(params struct {
    godi.In
    Handlers []EventHandler `group:"event-handlers"`
}) *EventDispatcher {
    dispatcher := &EventDispatcher{
        handlers: make(map[string][]EventHandler),
    }

    // Group handlers by event type
    for _, handler := range params.Handlers {
        eventType := handler.EventType()
        dispatcher.handlers[eventType] = append(
            dispatcher.handlers[eventType],
            handler,
        )
    }

    return dispatcher
}

func (d *EventDispatcher) Dispatch(event Event) error {
    handlers, ok := d.handlers[event.Type]
    if !ok {
        return nil // No handlers for this event
    }

    var errs []error
    for _, handler := range handlers {
        if err := handler.Handle(event); err != nil {
            errs = append(errs, err)
        }
    }

    if len(errs) > 0 {
        return fmt.Errorf("event handling errors: %v", errs)
    }

    return nil
}

Combining Groups with Keys

You can use both groups and keys together:

// Different validator groups
var ValidationModule = godi.NewModule("validation",
    // User validators
    godi.AddSingleton(NewEmailValidator,
        godi.Group("validators"),
        godi.Name("user-validators")),
    godi.AddSingleton(NewPasswordValidator,
        godi.Group("validators"),
        godi.Name("user-validators")),

    // Product validators
    godi.AddSingleton(NewPriceValidator,
        godi.Group("validators"),
        godi.Name("product-validators")),
    godi.AddSingleton(NewSKUValidator,
        godi.Group("validators"),
        godi.Name("product-validators")),
)

Best Practices

1. Order Matters

Services are injected in registration order:

// Middleware runs in this order: Auth -> RateLimit -> Logging
var MiddlewareModule = godi.NewModule("middleware",
    godi.AddScoped(NewAuthMiddleware, godi.Group("middleware")),
    godi.AddScoped(NewRateLimitMiddleware, godi.Group("middleware")),
    godi.AddScoped(NewLoggingMiddleware, godi.Group("middleware")),
)

2. Document Group Members

// Package middleware provides HTTP middleware.
//
// Available middleware (in execution order):
// 1. Authentication - Validates JWT tokens
// 2. Rate Limiting - Limits requests per IP
// 3. Logging - Logs all requests
// 4. CORS - Handles cross-origin requests
var MiddlewareModule = godi.NewModule("middleware",
    // ...
)

3. Empty Groups are OK

If no services are registered for a group, an empty slice is injected:

func NewPluginManager(params struct {
    godi.In
    Plugins []Plugin `group:"plugins"`
}) *PluginManager {
    // params.Plugins might be empty - that's fine
    return &PluginManager{plugins: params.Plugins}
}

4. Type Safety

Groups maintain type safety:

// This won't compile if any service in the group
// doesn't implement Validator
type MyService struct {
    validators []Validator
}

func NewMyService(params struct {
    godi.In
    Validators []Validator `group:"validators"`
}) *MyService {
    return &MyService{validators: params.Validators}
}

When to Use Groups

Use service groups for:

  • Plugin systems - Extensible functionality

  • Middleware chains - Ordered processing

  • Event handlers - Multiple handlers per event

  • Validators - Run all validations

  • Processors - Pipeline processing

  • Observers - Notification systems

Summary

Service groups are powerful for:

  • Collecting similar services

  • Building extensible systems

  • Creating processing pipelines

  • Implementing plugin architectures

The key is having a common interface and meaningful grouping!