Service Factories

This example demonstrates various service factory patterns in Lokstra, including simple factories, configuration-based factories, dependency injection, and lifecycle management.

Overview

Service factories are functions that create and initialize service instances. They provide flexibility in how services are instantiated and configured.

Topics Covered:


Factory Patterns

1. Simple Factory

Basic service instantiation with minimal configuration:

type SimpleService struct {
    name string
}

func NewSimpleService(name string) *SimpleService {
    return &SimpleService{name: name}
}

func SimpleServiceFactory(deps map[string]any, config map[string]any) any {
    name := "simple-service"
    if nameVal, ok := config["name"].(string); ok {
        name = nameVal
    }
    return NewSimpleService(name)
}

Registration:

lokstra_registry.RegisterServiceType("simple-service", SimpleServiceFactory, nil)
lokstra_registry.RegisterLazyService("simple-service", SimpleServiceFactory, map[string]any{
    "name": "My Simple Service",
})

Usage:

svc := lokstra_registry.GetService[*SimpleService]("simple-service")
info := svc.GetInfo()

2. Configurable Factory

Services that read configuration with defaults:

type ConfigurableService struct {
    apiKey     string
    maxRetries int
    timeout    time.Duration
}

func ConfigurableServiceFactory(deps map[string]any, config map[string]any) any {
    // Read with defaults
    apiKey := ""
    if key, ok := config["api_key"].(string); ok {
        apiKey = key
    }

    maxRetries := 3
    if retries, ok := config["max_retries"].(int); ok {
        maxRetries = retries
    }

    timeout := 30 * time.Second
    if timeoutVal, ok := config["timeout_seconds"].(int); ok {
        timeout = time.Duration(timeoutVal) * time.Second
    }

    return NewConfigurableService(apiKey, maxRetries, timeout)
}

Configuration:

lokstra_registry.RegisterLazyService("configurable-service", ConfigurableServiceFactory, map[string]any{
    "api_key":         "sk-test-12345",
    "max_retries":     5,
    "timeout_seconds": 60,
})

Benefits:


3. Factory with Dependencies

Services that depend on other services:

type DependentService struct {
    cache  *CacheService
    logger *LoggerService
}

func DependentServiceFactory(deps map[string]any, config map[string]any) any {
    cache := deps["cache-service"].(*service.Cached[*CacheService])
    logger := deps["logger-service"].(*service.Cached[*LoggerService])

    return NewDependentService(cache.Get(), logger.Get())
}

Registration with Dependencies:

lokstra_registry.RegisterLazyServiceWithDeps("dependent-service",
    DependentServiceFactory,
    map[string]string{
        "cache-service":  "cache-service",
        "logger-service": "logger-service",
    },
    nil, nil,
)

Dependency Resolution:


4. Lifecycle Management

Services with start/stop lifecycle:

type LifecycleService struct {
    name       string
    startTime  time.Time
    isRunning  bool
    background context.CancelFunc
}

func (s *LifecycleService) Start(ctx context.Context) error {
    if s.isRunning {
        return fmt.Errorf("service already running")
    }

    bgCtx, cancel := context.WithCancel(ctx)
    s.background = cancel
    s.isRunning = true

    // Start background task
    go func() {
        ticker := time.NewTicker(5 * time.Second)
        defer ticker.Stop()

        for {
            select {
            case <-bgCtx.Done():
                return
            case <-ticker.C:
                // Background work
            }
        }
    }()

    return nil
}

func (s *LifecycleService) Stop() error {
    if s.background != nil {
        s.background()
    }
    s.isRunning = false
    return nil
}

Factory with Initialization:

func LifecycleServiceFactory(deps map[string]any, config map[string]any) any {
    svc := NewLifecycleService(name)
    
    // Start service immediately
    if err := svc.Start(context.Background()); err != nil {
        log.Printf("Failed to start service: %v", err)
    }
    
    return svc
}

Lifecycle Patterns:


Factory Signature

All service factories must follow this signature:

func ServiceFactory(deps map[string]any, config map[string]any) any

Parameters:

Returns:


Best Practices

1. Use Constructor Functions

Create separate constructor functions for clarity:

func NewMyService(db *Database, logger *Logger) *MyService {
    return &MyService{
        db:     db,
        logger: logger,
    }
}

func MyServiceFactory(deps map[string]any, config map[string]any) any {
    return NewMyService(
        deps["db"].(*Database),
        deps["logger"].(*Logger),
    )
}

2. Validate Configuration Early

Fail fast if configuration is invalid:

func ServiceFactory(deps map[string]any, config map[string]any) any {
    apiKey, ok := config["api_key"].(string)
    if !ok || apiKey == "" {
        panic("api_key is required")
    }
    
    return NewService(apiKey)
}

3. Use Type-Safe Dependencies

Cast dependencies early for type safety:

func ServiceFactory(deps map[string]any, config map[string]any) any {
    // Type-safe casting
    cache, ok := deps["cache"].(*service.Cached[*CacheService])
    if !ok {
        panic("cache dependency not found")
    }
    
    return NewService(cache.Get())
}

4. Log Initialization

Log service creation for debugging:

func ServiceFactory(deps map[string]any, config map[string]any) any {
    fmt.Printf("✓ Creating MyService with config: %+v\n", config)
    return NewMyService(config)
}

Registration Methods

RegisterServiceType

Register a service type for use in YAML configuration:

lokstra_registry.RegisterServiceType(
    "my-service",           // Type name
    LocalFactory,           // Local factory
    RemoteFactory,          // Remote factory (optional)
)

RegisterLazyService

Register a service instance (no dependencies):

lokstra_registry.RegisterLazyService(
    "service-name",         // Service name
    ServiceFactory,         // Factory function
    map[string]any{         // Configuration
        "key": "value",
    },
)

RegisterLazyServiceWithDeps

Register a service with dependencies:

lokstra_registry.RegisterLazyServiceWithDeps(
    "service-name",         // Service name
    ServiceFactory,         // Factory function
    map[string]string{      // Dependencies (key → service name)
        "db":     "database",
        "cache":  "redis-cache",
    },
    map[string]any{         // Configuration
        "timeout": 30,
    },
    nil,                    // Options (optional)
)

Running the Example

cd docs/02-deep-dive/02-service/examples/01-service-factories
go run main.go

Test Endpoints:

# Simple factory
curl http://localhost:3000/simple

# Configurable factory
curl http://localhost:3000/configurable

# Dependent factory (with cache & logger)
curl -X POST http://localhost:3000/process \
  -H "Content-Type: application/json" \
  -d '{"key": "test", "value": "data"}'

# Lifecycle management
curl http://localhost:3000/lifecycle

Or use the test.http file for interactive testing.


Key Takeaways

  1. Factory Pattern - Clean separation between construction and usage
  2. Configuration - Type-safe config reading with defaults
  3. Dependencies - Framework handles dependency resolution
  4. Lifecycle - Services can manage their own start/stop logic
  5. Type Safety - Generic GetService[T] for type-safe retrieval


Status: ✅ Complete