Service Groups

Service groups allow you to collect multiple services of the same type into a slice. This is perfect for plugin architectures, middleware chains, or any scenario where you need to work with collections of services.

Basic Concept

Groups collect services registered with the same group name:

// Register multiple handlers in a group
services.AddSingleton(NewUserHandler, godi.Group("handlers"))
services.AddSingleton(NewProductHandler, godi.Group("handlers"))
services.AddSingleton(NewOrderHandler, godi.Group("handlers"))

// Consume all handlers as a slice
type Application struct {
    godi.In
    Handlers []Handler `group:"handlers"`
}

HTTP Handler Example

Building a modular HTTP application:

// Handler interface
type Handler interface {
    Pattern() string
    ServeHTTP(w http.ResponseWriter, r *http.Request)
}

// User handler
type UserHandler struct {
    userService UserService
}

func NewUserHandler(userService UserService) Handler {
    return &UserHandler{userService: userService}
}

func (h *UserHandler) Pattern() string {
    return "/users"
}

func (h *UserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // Handle user requests
}

// Product handler
type ProductHandler struct {
    productService ProductService
}

func NewProductHandler(productService ProductService) Handler {
    return &ProductHandler{productService: productService}
}

func (h *ProductHandler) Pattern() string {
    return "/products"
}

func (h *ProductHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // Handle product requests
}

// Register handlers
services.AddScoped(NewUserService)
services.AddScoped(NewProductService)
services.AddSingleton(NewUserHandler, godi.Group("routes"))
services.AddSingleton(NewProductHandler, godi.Group("routes"))

// Router that consumes all handlers
type Router struct {
    godi.In
    Routes []Handler `group:"routes"`
}

func NewHTTPServer(params Router) *http.ServeMux {
    mux := http.NewServeMux()

    for _, route := range params.Routes {
        mux.Handle(route.Pattern(), route)
    }

    return mux
}

Middleware Chain

Creating a middleware pipeline:

// Middleware interface
type Middleware interface {
    Wrap(next http.Handler) http.Handler
    Priority() int // Lower numbers run first
}

// Logging middleware
type LoggingMiddleware struct {
    logger Logger
}

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

func (m *LoggingMiddleware) Wrap(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        m.logger.Info("Request processed",
            "method", r.Method,
            "path", r.URL.Path,
            "duration", time.Since(start),
        )
    })
}

func (m *LoggingMiddleware) Priority() int { return 10 }

// Auth middleware
type AuthMiddleware struct {
    authService AuthService
}

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

func (m *AuthMiddleware) Wrap(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", http.StatusUnauthorized)
            return
        }
        next.ServeHTTP(w, r)
    })
}

func (m *AuthMiddleware) Priority() int { return 20 }

// Register middlewares
services.AddSingleton(NewLoggingMiddleware, godi.Group("middleware"))
services.AddSingleton(NewAuthMiddleware, godi.Group("middleware"))
services.AddSingleton(NewRateLimitMiddleware, godi.Group("middleware"))
services.AddSingleton(NewCORSMiddleware, godi.Group("middleware"))

// Middleware chain builder
type MiddlewareChain struct {
    godi.In
    Middlewares []Middleware `group:"middleware"`
}

func BuildMiddlewareChain(params MiddlewareChain, handler http.Handler) http.Handler {
    // Sort by priority
    sort.Slice(params.Middlewares, func(i, j int) bool {
        return params.Middlewares[i].Priority() < params.Middlewares[j].Priority()
    })

    // Apply in reverse order (innermost first)
    for i := len(params.Middlewares) - 1; i >= 0; i-- {
        handler = params.Middlewares[i].Wrap(handler)
    }

    return handler
}

Event Handlers

Event-driven architecture with groups:

// Event handler interface
type EventHandler interface {
    EventType() string
    Handle(ctx context.Context, event Event) error
}

// User created handler
type UserCreatedHandler struct {
    emailService EmailService
    logger       Logger
}

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

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

func (h *UserCreatedHandler) Handle(ctx context.Context, event Event) error {
    userEvent := event.(*UserCreatedEvent)

    // Send welcome email
    err := h.emailService.SendWelcomeEmail(userEvent.Email)
    if err != nil {
        h.logger.Error("Failed to send welcome email", err)
    }

    return nil
}

// Order placed handler
type OrderPlacedHandler struct {
    inventoryService InventoryService
    notifyService    NotificationService
}

func NewOrderPlacedHandler(
    inventoryService InventoryService,
    notifyService NotificationService,
) EventHandler {
    return &OrderPlacedHandler{
        inventoryService: inventoryService,
        notifyService:    notifyService,
    }
}

func (h *OrderPlacedHandler) EventType() string {
    return "order.placed"
}

func (h *OrderPlacedHandler) Handle(ctx context.Context, event Event) error {
    orderEvent := event.(*OrderPlacedEvent)

    // Update inventory
    if err := h.inventoryService.Reserve(orderEvent.Items); err != nil {
        return err
    }

    // Send notification
    return h.notifyService.NotifyOrderPlaced(orderEvent.OrderID)
}

// Register handlers
services.AddSingleton(NewUserCreatedHandler, godi.Group("event-handlers"))
services.AddSingleton(NewOrderPlacedHandler, godi.Group("event-handlers"))
services.AddSingleton(NewPaymentProcessedHandler, godi.Group("event-handlers"))

// Event dispatcher
type EventDispatcher struct {
    godi.In
    Handlers []EventHandler `group:"event-handlers"`
}

func NewEventBus(params EventDispatcher) *EventBus {
    bus := &EventBus{
        handlers: make(map[string][]EventHandler),
    }

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

    return bus
}

type EventBus struct {
    handlers map[string][]EventHandler
}

func (bus *EventBus) Publish(ctx context.Context, event Event) error {
    handlers, ok := bus.handlers[event.Type()]
    if !ok {
        return nil // No handlers for this event
    }

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

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

    return nil
}

Validation Rules

Composable validation system:

// Validation rule interface
type ValidationRule interface {
    Name() string
    Validate(value interface{}) error
}

// Required rule
type RequiredRule struct{}

func NewRequiredRule() ValidationRule {
    return &RequiredRule{}
}

func (r *RequiredRule) Name() string { return "required" }

func (r *RequiredRule) Validate(value interface{}) error {
    if value == nil || value == "" {
        return errors.New("field is required")
    }
    return nil
}

// Email rule
type EmailRule struct{}

func NewEmailRule() ValidationRule {
    return &EmailRule{}
}

func (r *EmailRule) Name() string { return "email" }

func (r *EmailRule) Validate(value interface{}) error {
    email, ok := value.(string)
    if !ok {
        return errors.New("value must be string")
    }

    if !strings.Contains(email, "@") {
        return errors.New("invalid email format")
    }

    return nil
}

// Register rules
services.AddSingleton(NewRequiredRule, godi.Group("validators"))
services.AddSingleton(NewEmailRule, godi.Group("validators"))
services.AddSingleton(NewMinLengthRule, godi.Group("validators"))
services.AddSingleton(NewMaxLengthRule, godi.Group("validators"))

// Validator service
type ValidatorParams struct {
    godi.In
    Rules []ValidationRule `group:"validators"`
}

type Validator struct {
    rules map[string]ValidationRule
}

func NewValidator(params ValidatorParams) *Validator {
    rules := make(map[string]ValidationRule)

    for _, rule := range params.Rules {
        rules[rule.Name()] = rule
    }

    return &Validator{rules: rules}
}

func (v *Validator) ValidateStruct(s interface{}) error {
    // Use reflection to validate struct fields
    // Apply rules based on struct tags
    // Example: `validate:"required,email"`
    return nil
}

Plugin System

Building a plugin architecture:

// Plugin interface
type Plugin interface {
    Name() string
    Version() string
    Initialize(app *Application) error
    Start(ctx context.Context) error
    Stop(ctx context.Context) error
}

// Analytics plugin
type AnalyticsPlugin struct {
    config AnalyticsConfig
    client AnalyticsClient
}

func NewAnalyticsPlugin(config AnalyticsConfig) Plugin {
    return &AnalyticsPlugin{
        config: config,
        client: NewAnalyticsClient(config),
    }
}

func (p *AnalyticsPlugin) Name() string    { return "analytics" }
func (p *AnalyticsPlugin) Version() string { return "1.0.0" }

func (p *AnalyticsPlugin) Initialize(app *Application) error {
    // Register routes, middleware, etc.
    app.RegisterMiddleware(p.trackingMiddleware())
    return nil
}

// Search plugin
type SearchPlugin struct {
    searchEngine SearchEngine
    indexer      Indexer
}

func NewSearchPlugin(config SearchConfig) Plugin {
    return &SearchPlugin{
        searchEngine: NewElasticSearch(config),
        indexer:      NewIndexer(config),
    }
}

func (p *SearchPlugin) Name() string    { return "search" }
func (p *SearchPlugin) Version() string { return "2.1.0" }

// Register plugins
services.AddSingleton(NewAnalyticsPlugin, godi.Group("plugins"))
services.AddSingleton(NewSearchPlugin, godi.Group("plugins"))
services.AddSingleton(NewCachePlugin, godi.Group("plugins"))

// Plugin manager
type PluginManager struct {
    godi.In
    Plugins []Plugin `group:"plugins"`
}

func NewApplication(params PluginManager) (*Application, error) {
    app := &Application{
        plugins: make(map[string]Plugin),
    }

    // Initialize all plugins
    for _, plugin := range params.Plugins {
        log.Printf("Loading plugin: %s v%s", plugin.Name(), plugin.Version())

        if err := plugin.Initialize(app); err != nil {
            return nil, fmt.Errorf("failed to initialize plugin %s: %w",
                plugin.Name(), err)
        }

        app.plugins[plugin.Name()] = plugin
    }

    return app, nil
}

type Application struct {
    plugins map[string]Plugin
}

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

    return nil
}

Observers and Listeners

Observer pattern with groups:

// Observer interface
type Observer interface {
    OnEvent(event interface{})
}

// Metrics observer
type MetricsObserver struct {
    metrics MetricsCollector
}

func NewMetricsObserver(metrics MetricsCollector) Observer {
    return &MetricsObserver{metrics: metrics}
}

func (o *MetricsObserver) OnEvent(event interface{}) {
    switch e := event.(type) {
    case RequestEvent:
        o.metrics.IncrementCounter("requests", e.Method, e.Path)
    case ErrorEvent:
        o.metrics.IncrementCounter("errors", e.Type)
    }
}

// Logging observer
type LoggingObserver struct {
    logger Logger
}

func NewLoggingObserver(logger Logger) Observer {
    return &LoggingObserver{logger: logger}
}

func (o *LoggingObserver) OnEvent(event interface{}) {
    o.logger.Info("Event occurred", "event", event)
}

// Register observers
services.AddSingleton(NewMetricsObserver, godi.Group("observers"))
services.AddSingleton(NewLoggingObserver, godi.Group("observers"))
services.AddSingleton(NewAuditObserver, godi.Group("observers"))

// Observable service
type ObservableService struct {
    godi.In
    Observers []Observer `group:"observers"`
}

type EventEmitter struct {
    observers []Observer
}

func NewEventEmitter(params ObservableService) *EventEmitter {
    return &EventEmitter{
        observers: params.Observers,
    }
}

func (e *EventEmitter) Emit(event interface{}) {
    for _, observer := range e.observers {
        // Run observers asynchronously
        go observer.OnEvent(event)
    }
}

Conditional Registration

Register in groups conditionally:

func ConfigureFeatures(services godi.ServiceCollection, features FeatureFlags) {
    // Always register core features
    services.AddSingleton(NewCoreFeature, godi.Group("features"))

    // Conditionally register features
    if features.IsEnabled("advanced-search") {
        services.AddSingleton(NewAdvancedSearchFeature, godi.Group("features"))
    }

    if features.IsEnabled("real-time-sync") {
        services.AddSingleton(NewRealTimeSyncFeature, godi.Group("features"))
    }

    if features.IsEnabled("ai-recommendations") {
        services.AddSingleton(NewAIRecommendationsFeature, godi.Group("features"))
    }
}

// Feature manager consumes whatever is registered
type FeatureManager struct {
    godi.In
    Features []Feature `group:"features"`
}

func (fm *FeatureManager) ListEnabledFeatures() []string {
    names := make([]string, len(fm.Features))
    for i, f := range fm.Features {
        names[i] = f.Name()
    }
    return names
}

Testing with Groups

Groups make testing modular components easy:

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

    // Register test middlewares with specific priorities
    services.AddSingleton(func() Middleware {
        return &TestMiddleware{name: "first", priority: 1}
    }, godi.Group("middleware"))

    services.AddSingleton(func() Middleware {
        return &TestMiddleware{name: "second", priority: 2}
    }, godi.Group("middleware"))

    services.AddSingleton(func() Middleware {
        return &TestMiddleware{name: "third", priority: 3}
    }, godi.Group("middleware"))

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

    // Verify middleware order
    var params MiddlewareChain
    provider.Invoke(func(p MiddlewareChain) {
        params = p
    })

    assert.Len(t, params.Middlewares, 3)

    // Test that they execute in correct order
    handler := BuildMiddlewareChain(params, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("handled"))
    }))

    // Make request and verify middleware execution order
    // ...
}

Best Practices

1. Use Meaningful Group Names

// Good
godi.Group("http-routes")
godi.Group("event-handlers")
godi.Group("validation-rules")

// Avoid
godi.Group("stuff")
godi.Group("things")

2. Document Group Members

// HealthChecker performs health checks.
// Register implementations with group:"health-checks"
type HealthChecker interface {
    Name() string
    Check(ctx context.Context) error
}

3. Handle Empty Groups

type ServiceParams struct {
    godi.In
    Handlers []Handler `group:"handlers"`
}

func NewService(params ServiceParams) *Service {
    if len(params.Handlers) == 0 {
        log.Warn("No handlers registered")
    }

    return &Service{handlers: params.Handlers}
}

4. Order Matters Sometimes

// When order is important, use a priority or order field
type OrderedHandler interface {
    Handler
    Order() int
}

// Sort before use
sort.Slice(handlers, func(i, j int) bool {
    return handlers[i].Order() < handlers[j].Order()
})

Summary

Service groups enable:

  • Plugin architectures

  • Middleware chains

  • Event handling systems

  • Modular applications

  • Feature toggles

  • Extensible systems

They provide a clean way to work with collections of services while maintaining type safety and dependency injection benefits.