app

package
v0.29.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 27, 2026 License: MIT Imports: 29 Imported by: 0

README

Dependency Injection Container

This package provides a dependency injection (DI) container for assembling and managing application components following Clean Architecture principles.

Overview

The DI container centralizes the creation and wiring of all application dependencies, including:

  • Infrastructure components (database, logger)
  • Repositories (data access layer)
  • Use cases (business logic layer)
  • HTTP servers and handlers

Key Features

1. Lazy Initialization

Components are only created when first accessed, improving startup time and memory usage.

container := app.NewContainer(cfg)
// Nothing is initialized yet

server, err := container.HTTPServer()
// Now database, repositories, use cases, and server are initialized
2. Singleton Pattern

Each component is initialized only once and reused for subsequent calls.

logger1 := container.Logger()
logger2 := container.Logger()
// logger1 == logger2 (same instance)
3. Error Handling

Initialization errors are captured and returned consistently:

server, err := container.HTTPServer()
if err != nil {
    // Handle initialization error
}
4. Clean Shutdown

The container provides a unified shutdown method to clean up all resources:

defer container.Shutdown(ctx)

Architecture

Dependency Graph
Container
├── Config (provided)
├── Logger
│   └── depends on: Config.LogLevel
├── Database
│   └── depends on: Config.DB*
├── TxManager
│   └── depends on: Database
├── Crypto Services
│   ├── KMS Provider
│   │   └── depends on: Config.KMS*
│   └── Encryption Service
│       └── depends on: KMS Provider
├── Repositories (by domain)
│   ├── AuthRepository
│   │   └── depends on: Database
│   ├── SecretsRepository
│   │   └── depends on: Database
│   ├── TransitRepository
│   │   └── depends on: Database
│   └── TokenizationRepository
│       └── depends on: Database
├── Use Cases (by domain)
│   ├── AuthUseCase
│   │   └── depends on: AuthRepository, Crypto
│   ├── SecretsUseCase
│   │   └── depends on: SecretsRepository, Crypto
│   ├── TransitUseCase
│   │   └── depends on: TransitRepository, Crypto
│   └── TokenizationUseCase
│       └── depends on: TokenizationRepository, Crypto
└── HTTP Server
    ├── depends on: Logger, Config
    └── depends on: All Use Cases
Layer Separation

The container enforces clean architecture by managing dependencies at each layer:

  1. Infrastructure Layer: Database connections, logger, transaction manager
  2. Data Layer: Repositories for data access
  3. Business Layer: Use cases with business logic
  4. Presentation Layer: HTTP handlers and API endpoints

Usage Examples

Starting the HTTP Server
func runServer(ctx context.Context) error {
    // Load configuration
    cfg := config.Load()

    // Create DI container
    container := app.NewContainer(cfg)

    // Get logger
    logger := container.Logger()
    logger.Info("starting server")

    // Ensure cleanup on exit
    defer closeContainer(container, logger)

    // Get HTTP server (initializes all dependencies)
    server, err := container.HTTPServer()
    if err != nil {
        return fmt.Errorf("failed to initialize HTTP server: %w", err)
    }

    // Start server
    return server.Start(ctx)
}

Testing

The container is designed to be easily testable:

Unit Testing the Container
func TestContainer(t *testing.T) {
    cfg := &config.Config{
        LogLevel: "info",
        // ... other config
    }

    container := app.NewContainer(cfg)
    logger := container.Logger()

    if logger == nil {
        t.Fatal("expected non-nil logger")
    }
}
Integration Testing with Container

For integration tests, you can create a container with test configuration:

func setupTestContainer(t *testing.T) *app.Container {
    cfg := &config.Config{
        DBConnectionString: "postgres://test:test@localhost:5432/test_db",
        LogLevel:          "debug",
    }

    container := app.NewContainer(cfg)
    t.Cleanup(func() {
        container.Shutdown(context.Background())
    })

    return container
}

Adding New Components

To add a new component to the container:

1. Add field to Container struct
type Container struct {
    // ... existing fields
    
    // New component
    orderUseCase     *orderUsecase.OrderUseCase
    orderUseCaseInit sync.Once
}
2. Add getter method
func (c *Container) OrderUseCase() (*orderUsecase.OrderUseCase, error) {
    var err error
    c.orderUseCaseInit.Do(func() {
        c.orderUseCase, err = c.initOrderUseCase()
        if err != nil {
            c.initErrors["orderUseCase"] = err
        }
    })
    if err != nil {
        return nil, err
    }
    if storedErr, exists := c.initErrors["orderUseCase"]; exists {
        return nil, storedErr
    }
    return c.orderUseCase, nil
}
3. Add initialization method
func (c *Container) initProductRepository() (productUsecase.ProductRepository, error) {
    db, err := c.DB()
    if err != nil {
        return nil, fmt.Errorf("failed to get database: %w", err)
    }
    
    return productRepository.NewProductRepository(db), nil
}

Benefits of This Approach

1. Centralized Dependency Management

All component wiring is in one place (internal/app/di.go), making it easy to understand and maintain the application structure.

2. Clean main.go

The main.go file is significantly simpler and focused on application flow rather than dependency wiring.

Before:

// 60+ lines of manual dependency wiring
db, err := database.Connect(...)
txManager := database.NewTxManager(db)

userRepo := userRepository.NewUserRepository(db)
outboxRepo := outboxRepository.NewOutboxEventRepository(db)

userUseCase, err := userUsecase.NewUserUseCase(txManager, userRepo, outboxRepo)
server := http.NewServer(db, cfg.ServerHost, cfg.ServerPort, cfg.ServerReadTimeout, cfg.ServerWriteTimeout, cfg.ServerIdleTimeout, logger)

After:

// Clean and simple
container := app.NewContainer(cfg)
server, err := container.HTTPServer()
3. Testability

The container can be easily tested and mocked for integration tests.

4. Consistency

All parts of the application (server, CLI commands, migrations) use the same dependency initialization logic.

5. Scalability

Adding new domains (orders, products, etc.) is straightforward - just add methods to the container.

6. Type Safety

All dependencies are type-checked at compile time, unlike reflection-based DI frameworks.

Alternative Approaches

This implementation uses manual dependency injection with a container pattern. Other approaches include:

  1. Google Wire: Code generation for compile-time DI
  2. Uber Fx: Runtime reflection-based DI framework
  3. Pure Manual DI: Direct construction in main.go (previous approach)

The current approach provides a good balance between:

  • Simplicity (no external DI framework)
  • Maintainability (centralized wiring)
  • Performance (no reflection)
  • Type safety (compile-time checking)

Best Practices

  1. Always use defer for cleanup: defer closeContainer(container, logger)
  2. Check initialization errors: Always check errors returned by container methods
  3. Use lazy initialization: Don't initialize components you don't need
  4. Keep interfaces: Continue using interfaces for all dependencies
  5. Test the container: Write tests for container initialization logic
  6. Document dependencies: Keep the dependency graph documentation updated

Thread Safety

The container uses sync.Once to ensure thread-safe lazy initialization. Multiple goroutines can safely call container methods concurrently.

Performance Considerations

  • Lazy initialization reduces startup time for commands that don't need all components
  • Singleton pattern prevents creating duplicate instances
  • No reflection ensures fast performance compared to reflection-based DI
  • Compile-time safety catches dependency errors at build time

Future Enhancements

Potential improvements for the container:

  1. Component lifecycle hooks: Add OnStart and OnStop hooks
  2. Health checks: Integrate health checking into the container
  3. Metrics: Add metrics for component initialization time
  4. Configuration validation: Validate configuration before initializing components
  5. Graceful degradation: Support optional dependencies that can fail gracefully

See also

Documentation

Overview

Package app provides dependency injection container for assembling application components.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Container

type Container struct {
	// contains filtered or unexported fields
}

Container holds all application dependencies with lazy initialization.

func NewContainer

func NewContainer(cfg *config.Config) *Container

NewContainer creates a new dependency injection container with the provided configuration.

func (*Container) AuditLogHandler

func (c *Container) AuditLogHandler(ctx context.Context) (*authHTTP.AuditLogHandler, error)

AuditLogHandler returns the HTTP handler for audit log operations.

func (*Container) AuditLogRepository

func (c *Container) AuditLogRepository(ctx context.Context) (authDomain.AuditLogRepository, error)

AuditLogRepository returns the audit log repository.

func (*Container) AuditLogUseCase

func (c *Container) AuditLogUseCase(ctx context.Context) (authUseCase.AuditLogUseCase, error)

AuditLogUseCase returns the audit log use case.

func (*Container) BusinessMetrics added in v0.3.0

func (c *Container) BusinessMetrics(ctx context.Context) (metrics.BusinessMetrics, error)

BusinessMetrics returns the business metrics recorder.

func (*Container) ClientHandler

func (c *Container) ClientHandler(ctx context.Context) (*authHTTP.ClientHandler, error)

ClientHandler returns the HTTP handler for client management operations.

func (*Container) ClientRepository

func (c *Container) ClientRepository(ctx context.Context) (authDomain.ClientRepository, error)

ClientRepository returns the client repository.

func (*Container) ClientUseCase

func (c *Container) ClientUseCase(ctx context.Context) (authUseCase.ClientUseCase, error)

ClientUseCase returns the client use case.

func (*Container) CompareSecret added in v0.29.0

func (c *Container) CompareSecret() authUseCase.CompareSecretFunc

CompareSecret returns the CompareSecretFunc closure bound to the production hasher.

func (*Container) Config

func (c *Container) Config() *config.Config

Config returns the application configuration.

func (*Container) CryptoHandler

func (c *Container) CryptoHandler(ctx context.Context) (*transitHTTP.CryptoHandler, error)

func (*Container) DB

func (c *Container) DB(ctx context.Context) (*sql.DB, error)

DB returns the database connection.

func (*Container) HTTPServer

func (c *Container) HTTPServer(ctx context.Context) (*http.Server, error)

HTTPServer returns the HTTP server instance.

func (*Container) HashSecret added in v0.29.0

func (c *Container) HashSecret() authUseCase.HashSecretFunc

HashSecret returns the HashSecretFunc closure bound to the production hasher.

func (*Container) KMSService added in v0.6.0

func (c *Container) KMSService() keyring.KMSService

KMSService returns the KMS service.

func (*Container) KekUseCase

func (c *Container) KekUseCase(ctx context.Context) (keyring.KekUseCase, error)

KekUseCase returns the KEK use case.

func (*Container) KeySigner added in v0.29.0

func (c *Container) KeySigner(ctx context.Context) (keyring.KeySigner, error)

KeySigner returns the keyring's signing capability for audit log operations.

func (*Container) Keyring added in v0.29.0

func (c *Container) Keyring(ctx context.Context) (keyring.Keyring, error)

Keyring returns the envelope-encryption keyring shared by all features.

func (*Container) Logger

func (c *Container) Logger() *slog.Logger

Logger returns the configured logger instance.

func (*Container) MasterKeyChain

func (c *Container) MasterKeyChain(ctx context.Context) (*keyring.MasterKeyChain, error)

MasterKeyChain returns the master key chain loaded from environment variables.

func (*Container) MetricsProvider added in v0.3.0

func (c *Container) MetricsProvider(ctx context.Context) (*metrics.Provider, error)

MetricsProvider returns the metrics provider for Prometheus export.

func (*Container) MetricsServer added in v0.14.0

func (c *Container) MetricsServer(ctx context.Context) (*http.MetricsServer, error)

MetricsServer returns the Metrics server instance.

func (*Container) PasswordHasher added in v0.29.0

func (c *Container) PasswordHasher() *pwdhash.PasswordHasher

PasswordHasher returns a process-singleton pwdhash.PasswordHasher configured with the production policy. Used by HashSecret and CompareSecret closures below.

func (*Container) SecretHandler

func (c *Container) SecretHandler(ctx context.Context) (*secretsHTTP.SecretHandler, error)

SecretHandler returns the HTTP handler for secret management operations.

func (*Container) SecretRepository

func (c *Container) SecretRepository(ctx context.Context) (secretsDomain.SecretRepository, error)

SecretRepository returns the secret repository.

func (*Container) SecretUseCase

func (c *Container) SecretUseCase(ctx context.Context) (secretsUseCase.SecretUseCase, error)

SecretUseCase returns the secret use case.

func (*Container) Shutdown

func (c *Container) Shutdown(ctx context.Context) error

Shutdown performs cleanup of all initialized resources.

func (*Container) TokenHandler

func (c *Container) TokenHandler(ctx context.Context) (*authHTTP.TokenHandler, error)

TokenHandler returns the HTTP handler for token operations.

func (*Container) TokenRepository

func (c *Container) TokenRepository(ctx context.Context) (authDomain.TokenRepository, error)

TokenRepository returns the token repository.

func (*Container) TokenUseCase

func (c *Container) TokenUseCase(ctx context.Context) (authUseCase.TokenUseCase, error)

TokenUseCase returns the token use case.

func (*Container) TokenizationHandler added in v0.4.0

func (c *Container) TokenizationHandler(ctx context.Context) (*tokenizationHTTP.TokenizationHandler, error)

func (*Container) TokenizationKeyHandler added in v0.4.0

func (c *Container) TokenizationKeyHandler(
	ctx context.Context,
) (*tokenizationHTTP.TokenizationKeyHandler, error)

func (*Container) TokenizationKeyRepository added in v0.4.0

func (c *Container) TokenizationKeyRepository(
	ctx context.Context,
) (tokenizationDomain.TokenizationKeyRepository, error)

func (*Container) TokenizationKeyUseCase added in v0.4.0

func (c *Container) TokenizationKeyUseCase(
	ctx context.Context,
) (tokenizationUseCase.TokenizationKeyUseCase, error)

func (*Container) TokenizationTokenRepository added in v0.4.0

func (c *Container) TokenizationTokenRepository(
	ctx context.Context,
) (tokenizationDomain.TokenRepository, error)

func (*Container) TokenizationUseCase added in v0.4.0

func (c *Container) TokenizationUseCase(
	ctx context.Context,
) (tokenizationUseCase.TokenizationUseCase, error)

func (*Container) TransitKeyHandler

func (c *Container) TransitKeyHandler(ctx context.Context) (*transitHTTP.TransitKeyHandler, error)

func (*Container) TransitKeyRepository

func (c *Container) TransitKeyRepository(ctx context.Context) (transitDomain.TransitKeyRepository, error)

func (*Container) TransitKeyUseCase

func (c *Container) TransitKeyUseCase(ctx context.Context) (transitUseCase.TransitKeyUseCase, error)

func (*Container) TxManager

func (c *Container) TxManager(ctx context.Context) (database.TxManager, error)

TxManager returns the transaction manager.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL