Modules

Modules provide a way to organize and group related service registrations. They promote code organization, reusability, and maintainability in larger applications.

What are Modules?

A module is a function that configures a set of related services. Modules can:

  • Group related services together

  • Be reused across different applications

  • Depend on other modules

  • Encapsulate configuration logic

Creating a Module

Basic Module

var DatabaseModule = godi.Module("database",
    godi.AddSingleton(NewDatabaseConfig),
    godi.AddSingleton(NewDatabaseConnection),
    godi.AddScoped(NewUnitOfWork),
    godi.AddScoped(NewUserRepository),
    godi.AddScoped(NewOrderRepository),
)

Module with Dependencies

var LoggingModule = godi.Module("logging",
    godi.AddSingleton(NewLogConfig),
    godi.AddSingleton(NewLogger),
)

var DatabaseModule = godi.Module("database",
    godi.AddModule(LoggingModule), // Depend on logging
    godi.AddSingleton(NewDatabaseConnection),
    godi.AddScoped(NewRepository),
)

Using Modules

Single Module

func main() {
    collection := godi.NewServiceCollection()

    // Add a module
    err := collection.AddModules(DatabaseModule)
    if err != nil {
        log.Fatal(err)
    }

    provider, _ := collection.BuildServiceProvider()
    defer provider.Close()
}

Multiple Modules

func main() {
    collection := godi.NewServiceCollection()

    // Add multiple modules
    err := collection.AddModules(
        CoreModule,
        DatabaseModule,
        CacheModule,
        APIModule,
    )
    if err != nil {
        log.Fatal(err)
    }

    provider, _ := collection.BuildServiceProvider()
    defer provider.Close()
}

Module Patterns

Layered Architecture

// Core layer - no dependencies
var CoreModule = godi.Module("core",
    godi.AddSingleton(NewConfig),
    godi.AddSingleton(NewLogger),
    godi.AddSingleton(NewMetrics),
)

// Infrastructure layer - depends on core
var InfrastructureModule = godi.Module("infrastructure",
    godi.AddModule(CoreModule),
    godi.AddSingleton(NewDatabase),
    godi.AddSingleton(NewCache),
    godi.AddSingleton(NewMessageQueue),
)

// Domain layer - depends on infrastructure
var DomainModule = godi.Module("domain",
    godi.AddModule(InfrastructureModule),
    godi.AddScoped(NewUserService),
    godi.AddScoped(NewOrderService),
    godi.AddScoped(NewProductService),
)

// API layer - depends on domain
var APIModule = godi.Module("api",
    godi.AddModule(DomainModule),
    godi.AddScoped(NewUserController),
    godi.AddScoped(NewOrderController),
    godi.AddScoped(NewProductController),
)

Feature Modules

// User feature module
var UserFeatureModule = godi.Module("user-feature",
    godi.AddScoped(NewUserRepository),
    godi.AddScoped(NewUserService),
    godi.AddScoped(NewUserController),
    godi.AddSingleton(NewUserValidator),
)

// Order feature module
var OrderFeatureModule = godi.Module("order-feature",
    godi.AddScoped(NewOrderRepository),
    godi.AddScoped(NewOrderService),
    godi.AddScoped(NewOrderController),
    godi.AddScoped(NewPaymentGateway),
)

// Combine features
var ApplicationModule = godi.Module("application",
    godi.AddModule(CoreModule),
    godi.AddModule(UserFeatureModule),
    godi.AddModule(OrderFeatureModule),
)

Environment-Specific Modules

// Development module
var DevelopmentModule = godi.Module("development",
    godi.AddSingleton(func() Cache { return NewMemoryCache() }),
    godi.AddSingleton(func() Database { return NewSQLiteDB() }),
    godi.AddSingleton(func() EmailService { return NewMockEmailService() }),
)

// Production module
var ProductionModule = godi.Module("production",
    godi.AddSingleton(func() Cache { return NewRedisCache() }),
    godi.AddSingleton(func() Database { return NewPostgresDB() }),
    godi.AddSingleton(func() EmailService { return NewSMTPEmailService() }),
)

// Select module based on environment
func GetEnvironmentModule(env string) func(ServiceCollection) error {
    switch env {
    case "production":
        return ProductionModule
    case "development":
        return DevelopmentModule
    default:
        return DevelopmentModule
    }
}

Advanced Module Techniques

Module with Configuration

func DatabaseModuleWithConfig(dbConfig DatabaseConfig) func(godi.ServiceCollection) error {
    return godi.Module("database",
        godi.AddSingleton(func() *DatabaseConfig { return &dbConfig }),
        godi.AddSingleton(NewDatabaseConnection),
        godi.AddScoped(NewRepository),
    )
}

// Usage
dbConfig := DatabaseConfig{
    Host:     "localhost",
    Port:     5432,
    Database: "myapp",
}

collection.AddModules(DatabaseModuleWithConfig(dbConfig))

Conditional Registration in Modules

func APIModuleWithFeatures(features FeatureFlags) func(godi.ServiceCollection) error {
    return func(collection godi.ServiceCollection) error {
        // Always register core API services
        collection.AddScoped(NewUserController)
        collection.AddScoped(NewProductController)

        // Conditionally register features
        if features.OrdersEnabled {
            collection.AddScoped(NewOrderController)
            collection.AddScoped(NewOrderService)
        }

        if features.AnalyticsEnabled {
            collection.AddScoped(NewAnalyticsController)
            collection.AddSingleton(NewAnalyticsService)
        }

        if features.AdminEnabled {
            collection.AddScoped(NewAdminController)
            collection.AddScoped(NewAdminService)
        }

        return nil
    }
}

Module Composition

// Base modules
var LoggingModule = godi.Module("logging",
    godi.AddSingleton(NewLogger),
)

var MetricsModule = godi.Module("metrics",
    godi.AddSingleton(NewMetricsCollector),
)

var TracingModule = godi.Module("tracing",
    godi.AddSingleton(NewTracer),
)

// Composite module
var ObservabilityModule = godi.Module("observability",
    godi.AddModule(LoggingModule),
    godi.AddModule(MetricsModule),
    godi.AddModule(TracingModule),
    godi.AddSingleton(NewObservabilityService),
)

Testing with Modules

Test Module

var TestModule = godi.Module("test",
    godi.AddSingleton(func() Database { return NewInMemoryDB() }),
    godi.AddSingleton(func() Cache { return NewMockCache() }),
    godi.AddSingleton(func() EmailService { return NewMockEmailService() }),
)

func TestUserService(t *testing.T) {
    collection := godi.NewServiceCollection()

    // Use test module instead of production modules
    collection.AddModules(
        TestModule,
        UserFeatureModule,
    )

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

    // Test with mocked dependencies
    userService, _ := godi.Resolve[*UserService](provider)
    // ... run tests
}

Module Override Pattern

func TestWithOverrides(t *testing.T) {
    collection := godi.NewServiceCollection()

    // Add production module
    collection.AddModules(ProductionModule)

    // Override specific services for testing
    collection.Replace(godi.Singleton, func() EmailService {
        return &MockEmailService{
            shouldFail: true, // Test error cases
        }
    })

    provider, _ := collection.BuildServiceProvider()
    // ... run tests
}

Module Organization

Directory Structure

internal/
├── modules/
│   ├── core.go
│   ├── infrastructure.go
│   ├── domain.go
│   └── api.go
├── services/
│   ├── user/
│   ├── order/
│   └── product/
└── main.go

Module File Example

// internal/modules/infrastructure.go
package modules

import (
    "github.com/junioryono/godi"
    "myapp/internal/infrastructure/cache"
    "myapp/internal/infrastructure/database"
    "myapp/internal/infrastructure/messaging"
)

var InfrastructureModule = godi.Module("infrastructure",
    // Database
    godi.AddSingleton(database.NewConfig),
    godi.AddSingleton(database.NewConnection),
    godi.AddScoped(database.NewTransaction),

    // Cache
    godi.AddSingleton(cache.NewRedisConfig),
    godi.AddSingleton(cache.NewRedisClient),

    // Messaging
    godi.AddSingleton(messaging.NewRabbitMQConfig),
    godi.AddSingleton(messaging.NewPublisher),
    godi.AddSingleton(messaging.NewSubscriber),
)

Best Practices

1. Single Responsibility

Each module should have a single, clear purpose:

// ✅ Good - focused modules
var AuthModule = godi.Module("auth", ...)
var PaymentModule = godi.Module("payment", ...)

// ❌ Bad - mixed concerns
var UtilityModule = godi.Module("utility",
    godi.AddSingleton(NewAuth),
    godi.AddSingleton(NewPayment),
    godi.AddSingleton(NewEmail),
)

2. Clear Dependencies

Make module dependencies explicit:

// ✅ Good - clear dependency chain
var AppModule = godi.Module("app",
    CoreModule,      // Explicit dependency
    DatabaseModule,  // Explicit dependency
    godi.AddScoped(NewAppService),
)

3. Avoid Circular Dependencies

// ❌ Bad - circular dependency
var ModuleA = godi.Module("A",
    godi.AddModule(ModuleB), // A depends on B
)

var ModuleB = godi.Module("B",
    godi.AddModule(ModuleA), // B depends on A - circular!
)

4. Document Module Purpose

// Package auth provides authentication and authorization services.
//
// The AuthModule includes:
// - JWT token generation and validation
// - User authentication service
// - Permission checking service
// - Password hashing utilities
//
// Dependencies:
// - CoreModule (for logging and configuration)
// - DatabaseModule (for user storage)
var AuthModule = godi.Module("auth",
    // ... registrations
)

5. Test Modules Independently

func TestAuthModule(t *testing.T) {
    collection := godi.NewServiceCollection()

    // Test module can be loaded
    err := collection.AddModules(AuthModule)
    assert.NoError(t, err)

    // Test module provides expected services
    provider, _ := collection.BuildServiceProvider()

    _, err = godi.Resolve[AuthService](provider)
    assert.NoError(t, err)
}

Module Patterns for Common Scenarios

Web Application Module

var WebModule = godi.Module("web",
    // Core web services
    godi.AddSingleton(NewRouter),
    godi.AddSingleton(NewMiddlewareChain),

    // Request-scoped services
    godi.AddScoped(NewRequestContext),
    godi.AddScoped(NewRequestLogger),

    // Controllers as groups
    godi.AddScoped(NewUserController, godi.Group("controllers")),
    godi.AddScoped(NewProductController, godi.Group("controllers")),

    // Middleware as groups
    godi.AddSingleton(NewAuthMiddleware, godi.Group("middleware")),
    godi.AddSingleton(NewLoggingMiddleware, godi.Group("middleware")),
)

Background Jobs Module

var JobsModule = godi.Module("jobs",
    // Job infrastructure
    godi.AddSingleton(NewJobScheduler),
    godi.AddSingleton(NewJobQueue),
    godi.AddScoped(NewJobExecutor),

    // Job handlers as groups
    godi.AddSingleton(NewEmailJob, godi.Group("jobs")),
    godi.AddSingleton(NewReportJob, godi.Group("jobs")),
    godi.AddSingleton(NewCleanupJob, godi.Group("jobs")),
)

Plugin System Module

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

// Create module that loads plugins
func PluginModule(pluginDir string) func(godi.ServiceCollection) error {
    return func(collection godi.ServiceCollection) error {
        // Scan plugin directory
        plugins, err := loadPlugins(pluginDir)
        if err != nil {
            return err
        }

        // Register each plugin
        for _, plugin := range plugins {
            p := plugin // capture
            collection.AddSingleton(func() Plugin { return p },
                godi.Group("plugins"))
        }

        return nil
    }
}

Summary

Modules provide powerful organization capabilities:

  • Group related services together

  • Reuse common configurations

  • Compose complex applications from simple parts

  • Test components in isolation

  • Manage dependencies explicitly

Use modules to keep your dependency injection configuration clean, maintainable, and testable as your application grows.