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