Advanced Patterns

This guide covers advanced dependency injection patterns and techniques for complex scenarios.

Factory Pattern

Service Factory

Create services dynamically based on runtime parameters:

// Factory interface
type ServiceFactory interface {
    CreateService(serviceType string) (Service, error)
}

// Factory implementation
type serviceFactory struct {
    provider godi.ServiceProvider
    logger   Logger
}

func NewServiceFactory(provider godi.ServiceProvider, logger Logger) ServiceFactory {
    return &serviceFactory{
        provider: provider,
        logger:   logger,
    }
}

func (f *serviceFactory) CreateService(serviceType string) (Service, error) {
    switch serviceType {
    case "email":
        return godi.Resolve[EmailService](f.provider)
    case "sms":
        return godi.Resolve[SMSService](f.provider)
    case "push":
        return godi.Resolve[PushService](f.provider)
    default:
        return nil, fmt.Errorf("unknown service type: %s", serviceType)
    }
}

// Usage
factory, _ := godi.Resolve[ServiceFactory](provider)
service, _ := factory.CreateService("email")

Abstract Factory

Create families of related services:

// Abstract factory for different environments
type EnvironmentFactory interface {
    CreateDatabase() Database
    CreateCache() Cache
    CreateQueue() Queue
}

// Development factory
type developmentFactory struct {
    provider godi.ServiceProvider
}

func NewDevelopmentFactory(provider godi.ServiceProvider) EnvironmentFactory {
    return &developmentFactory{provider: provider}
}

func (f *developmentFactory) CreateDatabase() Database {
    db, _ := godi.ResolveKeyed[Database](f.provider, "sqlite")
    return db
}

func (f *developmentFactory) CreateCache() Cache {
    cache, _ := godi.ResolveKeyed[Cache](f.provider, "memory")
    return cache
}

func (f *developmentFactory) CreateQueue() Queue {
    queue, _ := godi.ResolveKeyed[Queue](f.provider, "memory")
    return queue
}

// Production factory
type productionFactory struct {
    provider godi.ServiceProvider
}

func (f *productionFactory) CreateDatabase() Database {
    db, _ := godi.ResolveKeyed[Database](f.provider, "postgres")
    return db
}

func (f *productionFactory) CreateCache() Cache {
    cache, _ := godi.ResolveKeyed[Cache](f.provider, "redis")
    return cache
}

func (f *productionFactory) CreateQueue() Queue {
    queue, _ := godi.ResolveKeyed[Queue](f.provider, "rabbitmq")
    return queue
}

Strategy Pattern

Dynamic Strategy Selection

// Strategy interface
type PaymentStrategy interface {
    ProcessPayment(amount float64) error
}

// Context that uses strategies
type PaymentProcessor struct {
    strategies map[string]PaymentStrategy
    logger     Logger
}

func NewPaymentProcessor(provider godi.ServiceProvider, logger Logger) *PaymentProcessor {
    return &PaymentProcessor{
        strategies: map[string]PaymentStrategy{
            "credit_card": resolveStrategy[CreditCardStrategy](provider),
            "paypal":      resolveStrategy[PayPalStrategy](provider),
            "crypto":      resolveStrategy[CryptoStrategy](provider),
        },
        logger: logger,
    }
}

func resolveStrategy[T PaymentStrategy](provider godi.ServiceProvider) PaymentStrategy {
    strategy, _ := godi.Resolve[T](provider)
    return strategy
}

func (p *PaymentProcessor) Process(method string, amount float64) error {
    strategy, ok := p.strategies[method]
    if !ok {
        return fmt.Errorf("unknown payment method: %s", method)
    }

    p.logger.Info("Processing payment", "method", method, "amount", amount)
    return strategy.ProcessPayment(amount)
}

Chain of Responsibility

Middleware Chain

// Handler interface
type Handler interface {
    Handle(ctx context.Context, request Request) (Response, error)
}

// Middleware interface
type Middleware interface {
    Process(next Handler) Handler
}

// Chain builder
type ChainBuilder struct {
    godi.In

    Middlewares []Middleware `group:"middleware"`
}

func BuildChain(params ChainBuilder, final Handler) Handler {
    // Sort middlewares by priority
    sort.Slice(params.Middlewares, func(i, j int) bool {
        return getPriority(params.Middlewares[i]) < getPriority(params.Middlewares[j])
    })

    // Build chain in reverse order
    handler := final
    for i := len(params.Middlewares) - 1; i >= 0; i-- {
        handler = params.Middlewares[i].Process(handler)
    }

    return handler
}

// Example middleware
type loggingMiddleware struct {
    logger Logger
}

func (m *loggingMiddleware) Process(next Handler) Handler {
    return HandlerFunc(func(ctx context.Context, req Request) (Response, error) {
        m.logger.Info("Processing request", "id", req.ID)

        resp, err := next.Handle(ctx, req)

        if err != nil {
            m.logger.Error("Request failed", "id", req.ID, "error", err)
        } else {
            m.logger.Info("Request succeeded", "id", req.ID)
        }

        return resp, err
    })
}

Lazy Initialization

Lazy Service Resolution

// Lazy wrapper for expensive services
type Lazy[T any] struct {
    provider godi.ServiceProvider
    once     sync.Once
    value    T
    err      error
}

func NewLazy[T any](provider godi.ServiceProvider) *Lazy[T] {
    return &Lazy[T]{
        provider: provider,
    }
}

func (l *Lazy[T]) Get() (T, error) {
    l.once.Do(func() {
        l.value, l.err = godi.Resolve[T](l.provider)
    })

    return l.value, l.err
}

// Usage in service
type MyService struct {
    expensiveService *Lazy[ExpensiveService]
}

func NewMyService(provider godi.ServiceProvider) *MyService {
    return &MyService{
        expensiveService: NewLazy[ExpensiveService](provider),
    }
}

func (s *MyService) DoWork() error {
    // Service is only created when needed
    expensive, err := s.expensiveService.Get()
    if err != nil {
        return err
    }

    return expensive.Process()
}

Service Locator (Anti-)Pattern

While generally discouraged, sometimes useful:

// Service locator for legacy code integration
type ServiceLocator struct {
    provider godi.ServiceProvider
}

func NewServiceLocator(provider godi.ServiceProvider) *ServiceLocator {
    return &ServiceLocator{provider: provider}
}

func (sl *ServiceLocator) Get(serviceType reflect.Type) (interface{}, error) {
    return sl.provider.Resolve(serviceType)
}

func (sl *ServiceLocator) GetKeyed(serviceType reflect.Type, key string) (interface{}, error) {
    return sl.provider.ResolveKeyed(serviceType, key)
}

// Generic version
func Get[T any](sl *ServiceLocator) (T, error) {
    return godi.Resolve[T](sl.provider)
}

// Use sparingly, prefer constructor injection
var Locator *ServiceLocator

func InitializeLocator(provider godi.ServiceProvider) {
    Locator = NewServiceLocator(provider)
}

Composite Pattern

Composite Services

// Notification service that combines multiple channels
type CompositeNotificationService struct {
    channels []NotificationChannel
    logger   Logger
}

type NotificationChannelParams struct {
    godi.In

    Channels []NotificationChannel `group:"notifications"`
    Logger   Logger
}

func NewCompositeNotificationService(params NotificationChannelParams) NotificationService {
    return &CompositeNotificationService{
        channels: params.Channels,
        logger:   params.Logger,
    }
}

func (s *CompositeNotificationService) Send(notification Notification) error {
    var errors []error

    for _, channel := range s.channels {
        if err := channel.Send(notification); err != nil {
            s.logger.Error("Channel failed", "channel", channel.Name(), "error", err)
            errors = append(errors, err)
        }
    }

    if len(errors) > 0 {
        return fmt.Errorf("notification failed on %d channels", len(errors))
    }

    return nil
}

// Register channels
collection.AddSingleton(NewEmailChannel, godi.Group("notifications"))
collection.AddSingleton(NewSMSChannel, godi.Group("notifications"))
collection.AddSingleton(NewPushChannel, godi.Group("notifications"))
collection.AddSingleton(NewCompositeNotificationService)

Proxy Pattern

Service Proxy

// Proxy for adding behavior without modifying the service
type CachingProxy struct {
    service UserService
    cache   Cache
    ttl     time.Duration
}

func NewCachingProxy(service UserService, cache Cache) UserService {
    return &CachingProxy{
        service: service,
        cache:   cache,
        ttl:     5 * time.Minute,
    }
}

func (p *CachingProxy) GetUser(ctx context.Context, id string) (*User, error) {
    // Check cache
    cacheKey := fmt.Sprintf("user:%s", id)
    if cached, found := p.cache.Get(cacheKey); found {
        return cached.(*User), nil
    }

    // Call real service
    user, err := p.service.GetUser(ctx, id)
    if err != nil {
        return nil, err
    }

    // Cache result
    p.cache.Set(cacheKey, user, p.ttl)

    return user, nil
}

// Register with decoration
collection.AddScoped(NewUserService)
collection.Decorate(func(service UserService, cache Cache) UserService {
    return NewCachingProxy(service, cache)
})

Observer Pattern

Event System

// Event system with DI
type EventBus struct {
    handlers map[string][]EventHandler
    mu       sync.RWMutex
}

type EventHandlerParams struct {
    godi.In

    Handlers []EventHandler `group:"events"`
}

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

    // Register all handlers
    for _, handler := range params.Handlers {
        for _, eventType := range handler.EventTypes() {
            bus.Subscribe(eventType, handler)
        }
    }

    return bus
}

func (eb *EventBus) Subscribe(eventType string, handler EventHandler) {
    eb.mu.Lock()
    defer eb.mu.Unlock()

    eb.handlers[eventType] = append(eb.handlers[eventType], handler)
}

func (eb *EventBus) Publish(event Event) error {
    eb.mu.RLock()
    handlers := eb.handlers[event.Type()]
    eb.mu.RUnlock()

    for _, handler := range handlers {
        if err := handler.Handle(event); err != nil {
            return err
        }
    }

    return nil
}

Command Pattern

Command Execution

// Command interface
type Command interface {
    Execute(ctx context.Context) error
    Name() string
}

// Command dispatcher
type CommandDispatcher struct {
    commands map[string]Command
    logger   Logger
}

type CommandParams struct {
    godi.In

    Commands []Command `group:"commands"`
    Logger   Logger
}

func NewCommandDispatcher(params CommandParams) *CommandDispatcher {
    dispatcher := &CommandDispatcher{
        commands: make(map[string]Command),
        logger:   params.Logger,
    }

    for _, cmd := range params.Commands {
        dispatcher.commands[cmd.Name()] = cmd
    }

    return dispatcher
}

func (d *CommandDispatcher) Dispatch(ctx context.Context, commandName string) error {
    cmd, ok := d.commands[commandName]
    if !ok {
        return fmt.Errorf("unknown command: %s", commandName)
    }

    d.logger.Info("Executing command", "name", commandName)

    start := time.Now()
    err := cmd.Execute(ctx)
    duration := time.Since(start)

    if err != nil {
        d.logger.Error("Command failed", "name", commandName, "error", err, "duration", duration)
        return err
    }

    d.logger.Info("Command succeeded", "name", commandName, "duration", duration)
    return nil
}

Unit of Work Pattern

Transaction Management

// Unit of Work for managing database transactions
type UnitOfWork interface {
    UserRepository() UserRepository
    OrderRepository() OrderRepository
    Commit() error
    Rollback() error
}

type unitOfWork struct {
    tx       *sql.Tx
    userRepo UserRepository
    orderRepo OrderRepository
    committed bool
}

func NewUnitOfWork(db *sql.DB) (UnitOfWork, error) {
    tx, err := db.Begin()
    if err != nil {
        return nil, err
    }

    return &unitOfWork{
        tx:        tx,
        userRepo:  NewTxUserRepository(tx),
        orderRepo: NewTxOrderRepository(tx),
    }, nil
}

func (uow *unitOfWork) UserRepository() UserRepository {
    return uow.userRepo
}

func (uow *unitOfWork) OrderRepository() OrderRepository {
    return uow.orderRepo
}

func (uow *unitOfWork) Commit() error {
    if uow.committed {
        return errors.New("already committed")
    }

    uow.committed = true
    return uow.tx.Commit()
}

func (uow *unitOfWork) Rollback() error {
    if uow.committed {
        return errors.New("already committed")
    }

    return uow.tx.Rollback()
}

// Service using Unit of Work
type OrderService struct {
    db     *sql.DB
    logger Logger
}

func (s *OrderService) CreateOrder(ctx context.Context, order *Order) error {
    uow, err := NewUnitOfWork(s.db)
    if err != nil {
        return err
    }
    defer uow.Rollback() // Rollback if not committed

    // Update user
    user, err := uow.UserRepository().GetByID(ctx, order.UserID)
    if err != nil {
        return err
    }

    user.OrderCount++
    if err := uow.UserRepository().Update(ctx, user); err != nil {
        return err
    }

    // Create order
    if err := uow.OrderRepository().Create(ctx, order); err != nil {
        return err
    }

    // Commit transaction
    return uow.Commit()
}

Specification Pattern

Dynamic Queries

// Specification interface
type Specification[T any] interface {
    IsSatisfiedBy(item T) bool
    And(other Specification[T]) Specification[T]
    Or(other Specification[T]) Specification[T]
    Not() Specification[T]
}

// Base specification
type baseSpec[T any] struct {
    predicate func(T) bool
}

func (s *baseSpec[T]) IsSatisfiedBy(item T) bool {
    return s.predicate(item)
}

func (s *baseSpec[T]) And(other Specification[T]) Specification[T] {
    return &baseSpec[T]{
        predicate: func(item T) bool {
            return s.IsSatisfiedBy(item) && other.IsSatisfiedBy(item)
        },
    }
}

// User specifications
func UserIsActive() Specification[User] {
    return &baseSpec[User]{
        predicate: func(u User) bool {
            return u.Status == "active"
        },
    }
}

func UserHasRole(role string) Specification[User] {
    return &baseSpec[User]{
        predicate: func(u User) bool {
            return u.Role == role
        },
    }
}

// Repository using specifications
type SpecificationRepository[T any] interface {
    Find(spec Specification[T]) ([]T, error)
}

// Usage
activeAdmins := UserIsActive().And(UserHasRole("admin"))
users, err := repo.Find(activeAdmins)

Summary

These advanced patterns enable:

  • Flexible architectures with factories and strategies

  • Clean abstractions with proxies and decorators

  • Complex workflows with command and unit of work patterns

  • Dynamic behavior with specifications and observers

Use these patterns judiciously - they add complexity but can greatly improve maintainability and flexibility in large applications.