Documentation

Dependency Injection Architecture

Overview

The framework uses explicit dependency injection through a centralized container pattern. All services and their dependencies are wired explicitly, providing compile-time safety and clear dependency graphs.

Container Pattern

Main Container Structure

type Container struct {
    // Core infrastructure
    SqlDb  *sql_db.SqlDb
    Router *gin.Engine
    Config *config.Config
    
    // Shared utilities
    Fetcher    *fetcher.Fetcher
    Handler    *handler.Handler
    Hydrator   *hydrator.Hydrator
    Relationer *relationer.Relationer
    Searcher   *searcher.Searcher
    Spy        *spy.Spy
    Time       time.Time
    
    // Entity Tables
    GardenTable    *tables.GardenTable
    UserTable      *tables.UserTable
    PlantTable     *tables.PlantTable
    
    // Entity Services
    GardenFetcher    *fetchers.GardenFetcher
    GardenHandler    *handlers.GardenHandler
    GardenHydrator   *hydrators.GardenHydrator
    GardenMolder     *molders.CreateGardenFormMolder
    GardenSerializer *serializers.GardenSerializer
    
    // Business Logic
    CreateGardenMae *mae.CreateGardenMae
    UpdateGardenMae *mae.UpdateGardenMae
    
    // Controllers
    GardenController *controllers.GardenController
    
    // HTTP Components
    AppRouter *routers.AppRouter
    ApiRouter *routers.ApiRouter
}

Container Building

func (c *Container) Build() {
    // Phase 1: Infrastructure
    c.buildInfrastructure()
    
    // Phase 2: Tables
    c.buildTables()
    
    // Phase 3: Services
    c.buildServices()
    
    // Phase 4: Business Logic
    c.buildMaestros()
    
    // Phase 5: Controllers
    c.buildControllers()
    
    // Phase 6: Routers
    c.buildRouters()
}

Dependency Wiring Phases

Phase 1: Infrastructure

func (c *Container) buildInfrastructure() {
    // Database
    c.SqlDb = sql_db.NewSqlDb(c.Config.Database)
    
    // Core utilities
    c.Fetcher = &fetcher.Fetcher{SqlDb: c.SqlDb}
    c.Handler = &handler.Handler{
        SqlDb: c.SqlDb,
        Spy:   c.Spy,
        Time:  c.Time,
    }
    c.Hydrator = &hydrator.Hydrator{}
    c.Relationer = &relationer.Relationer{}
    c.Searcher = &searcher.Searcher{}
    
    // Monitoring and utilities
    c.Spy = &spy.Spy{}
    c.Time = time.NewTime()
}

Phase 2: Tables

func (c *Container) buildTables() {
    // Entity table services
    c.GardenTable = &tables.GardenTable{}
    c.UserTable = &tables.UserTable{}
    c.PlantTable = &tables.PlantTable{}
    
    // Table services don't have dependencies
    // They provide mapping functionality
}

Phase 3: Services

func (c *Container) buildServices() {
    // Garden services
    c.GardenFetcher = &fetchers.GardenFetcher{
        Fetcher:     c.Fetcher,
        GardenTable: c.GardenTable,
    }
    
    c.GardenHandler = &handlers.GardenHandler{
        Handler:     c.Handler,
        GardenTable: c.GardenTable,
    }
    
    c.GardenHydrator = &hydrators.GardenHydrator{
        GardenRelationer: c.GardenRelationer,
        Hydrator:         c.Hydrator,
    }
    
    c.GardenSerializer = &serializers.GardenSerializer{}
    
    c.GardenMolder = &molders.CreateGardenFormMolder{}
    
    // Repeat pattern for other entities...
}

Phase 4: Business Logic

func (c *Container) buildMaestros() {
    c.CreateGardenMae = &mae.CreateGardenMae{
        GardenFetcher:    c.GardenFetcher,
        GardenHandler:    c.GardenHandler,
        GardenMolder:     c.GardenMolder,
        GardenValidator:  c.GardenValidator,
        UserFetcher:      c.UserFetcher,
        Logger:           c.Logger,
        Time:             c.Time,
    }
    
    c.UpdateGardenMae = &mae.UpdateGardenMae{
        GardenFetcher:       c.GardenFetcher,
        GardenHandler:       c.GardenHandler,
        UpdateGardenMolder:  c.UpdateGardenMolder,
        GardenValidator:     c.GardenValidator,
        Logger:              c.Logger,
        Time:                c.Time,
    }
}

Phase 5: Controllers

func (c *Container) buildControllers() {
    c.GardenController = &controllers.GardenController{
        CreateGardenMae:   c.CreateGardenMae,
        UpdateGardenMae:   c.UpdateGardenMae,
        GardenFetcher:     c.GardenFetcher,
        GardenHydrator:    c.GardenHydrator,
        GardenSerializer:  c.GardenSerializer,
        GardenBinder:      c.GardenBinder,
        GardenPresenter:   c.GardenPresenter,
    }
}

Phase 6: Routers

func (c *Container) buildRouters() {
    c.AppRouter = &routers.AppRouter{
        Router:           c.Router,
        GardenController: c.GardenController,
        UserController:   c.UserController,
        PlantController:  c.PlantController,
    }
    
    c.ApiRouter = &routers.ApiRouter{
        Router:           c.Router,
        GardenController: c.GardenController,
        UserController:   c.UserController,
    }
}

Dependency Injection Principles

Explicit Wiring

All dependencies must be explicitly declared and wired:

// GOOD: Explicit dependency injection
type CreateGardenMae struct {
    GardenFetcher   *fetchers.GardenFetcher   // Explicitly injected
    GardenHandler   *handlers.GardenHandler   // Explicitly injected
    GardenValidator *validators.GardenValidator // Explicitly injected
    Logger          *logger.Logger            // Explicitly injected
}

// BAD: Hidden dependencies
type CreateGardenMae struct {
    // No declared dependencies, uses globals or service locator
}

Constructor Injection

// Services receive dependencies through their structure
func NewGardenFetcher(fetcher *fetcher.Fetcher, table *tables.GardenTable) *GardenFetcher {
    return &GardenFetcher{
        Fetcher:     fetcher,
        GardenTable: table,
    }
}

// Or direct struct initialization in container
c.GardenFetcher = &fetchers.GardenFetcher{
    Fetcher:     c.Fetcher,
    GardenTable: c.GardenTable,
}

Interface Dependencies

// Define interfaces for external dependencies
type DatabaseProvider interface {
    Query(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)
    Exec(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
}

type GardenService struct {
    DB     DatabaseProvider  // Interface dependency
    Logger LoggerInterface   // Interface dependency
}

Container Initialization

Application Bootstrap

// main.go
func main() {
    // Load configuration
    config := config.Load()
    
    // Create container
    container := &Container{
        Config: config,
    }
    
    // Build all dependencies
    container.Build()
    
    // Validate dependencies
    if err := container.Validate(); err != nil {
        log.Fatal("Container validation failed:", err)
    }
    
    // Start application
    app := app.NewApp(container)
    app.Start()
}

Container Validation

func (c *Container) Validate() error {
    if c.SqlDb == nil {
        return errors.New("SqlDb is required")
    }
    
    if c.GardenFetcher == nil {
        return errors.New("GardenFetcher is required")
    }
    
    if c.CreateGardenMae == nil {
        return errors.New("CreateGardenMae is required")
    }
    
    // Validate service dependencies
    if c.CreateGardenMae.GardenFetcher == nil {
        return errors.New("CreateGardenMae.GardenFetcher is required")
    }
    
    return nil
}

Environment-Specific Containers

Development Container

func (c *Container) buildDevelopmentServices() {
    // Development-specific services
    c.DevMiddleware = &middleware.DevMiddleware{
        Logger: c.Logger,
        Config: c.Config,
    }
    
    c.HotReloader = &dev.HotReloader{
        Config: c.Config,
    }
    
    // Override production services with dev versions
    c.EmailService = &dev.MockEmailService{}
    c.PaymentService = &dev.MockPaymentService{}
}

Test Container

func BuildTestContainer(t *testing.T) *Container {
    config := &config.Config{
        Environment: "test",
        Database: config.DatabaseConfig{
            Driver: "sqlite3",
            Path:   ":memory:",
        },
    }
    
    container := &Container{
        Config: config,
    }
    
    // Build with test-specific overrides
    container.Build()
    container.buildTestServices()
    
    return container
}

func (c *Container) buildTestServices() {
    // Test-specific overrides
    c.Time = &test.MockTime{}
    c.EmailService = &test.MockEmailService{}
    c.ExternalAPI = &test.MockExternalAPI{}
}

Circular Dependency Resolution

Avoiding Circular Dependencies

// BAD: Circular dependency
type UserService struct {
    OrderService *OrderService  // UserService depends on OrderService
}

type OrderService struct {
    UserService *UserService   // OrderService depends on UserService
}

// GOOD: Extract shared interface
type UserProvider interface {
    FindUserById(id string) (*User, error)
}

type OrderProvider interface {
    FindOrdersByUserId(userId string) ([]*Order, error)
}

type UserService struct {
    OrderProvider OrderProvider  // Depends on interface
}

type OrderService struct {
    UserProvider UserProvider   // Depends on interface
}

Interface Segregation

// Define minimal interfaces for dependencies
type GardenReader interface {
    FindOneById(ctx context.Context, id string) (*mdl.Garden, bool, error)
}

type GardenWriter interface {
    Create(ctx context.Context, garden *mdl.Garden) (*mdl.Garden, error)
    Update(ctx context.Context, garden *mdl.Garden) (*mdl.Garden, error)
}

// Services depend on minimal interfaces
type PlantService struct {
    Gardens GardenReader  // Only needs read access
}

type GardenMaintenance struct {
    Gardens GardenWriter  // Only needs write access
}

Testing with Dependency Injection

Mock Dependencies

func TestCreateGardenMae(t *testing.T) {
    // Create mock dependencies
    mockFetcher := &mocks.GardenFetcher{}
    mockHandler := &mocks.GardenHandler{}
    mockValidator := &mocks.GardenValidator{}
    
    // Configure mocks
    mockValidator.On("ValidateCreateForm", mock.Anything).Return(nil)
    mockHandler.On("Create", mock.Anything, mock.Anything, mock.Anything).Return(&mdl.Garden{Id: "123"}, nil)
    
    // Create Mae with mocked dependencies
    mae := &mae.CreateGardenMae{
        GardenFetcher:   mockFetcher,
        GardenHandler:   mockHandler,
        GardenValidator: mockValidator,
    }
    
    // Test execution
    result, err := mae.Execute(ctx, input)
    
    // Verify results and mock calls
    require.NoError(t, err)
    mockValidator.AssertExpectations(t)
    mockHandler.AssertExpectations(t)
}

Integration Testing

func TestGardenWorkflow_Integration(t *testing.T) {
    // Use real container with test database
    container := BuildTestContainer(t)
    
    // Test complete workflow
    createMae := container.CreateGardenMae
    updateMae := container.UpdateGardenMae
    fetcher := container.GardenFetcher
    
    // Execute workflow
    garden, err := createMae.Execute(ctx, createInput)
    require.NoError(t, err)
    
    updatedGarden, err := updateMae.Execute(ctx, updateInput)
    require.NoError(t, err)
    
    foundGarden, exists, err := fetcher.FindOneById(ctx, garden.Id, nil)
    require.NoError(t, err)
    assert.True(t, exists)
}

Performance Considerations

Lazy Initialization

// For expensive resources, use lazy initialization
func (c *Container) GetSearchService() *SearchService {
    if c.searchService == nil {
        c.searchService = &SearchService{
            Index:  c.buildSearchIndex(),
            Config: c.Config.Search,
        }
    }
    return c.searchService
}

Singleton vs Instance Management

// Singleton services (shared state)
type CacheService struct {
    cache map[string]interface{}
    mutex sync.RWMutex
}

// Instance services (no shared state)
type ValidationService struct {
    rules []ValidationRule
}

// In container - singleton
c.CacheService = &CacheService{cache: make(map[string]interface{})}

// In container - can be instance per use
c.ValidationService = &ValidationService{rules: defaultRules}

LLM Development Notes

When working with dependency injection: 1. Explicit Dependencies - Always declare all dependencies in service structs 2. Container Building - Follow the phase-based building pattern 3. Interface Dependencies - Use interfaces to break circular dependencies 4. Testing Strategy - Use mocks for unit tests, real container for integration 5. Validation - Always validate container after building 6. Environment Handling - Use different container configurations per environment 7. Performance - Consider lazy initialization for expensive resources