Service

Service utilities and dependency injection helpers

Overview

The service package provides utilities for lazy-loading services with type-safe dependency injection. The core type is Cached[T], which enables lazy initialization, automatic caching, and thread-safe access to services.

Import Path

import "github.com/primadi/lokstra/core/service"

Core Type

Cached[T]

Type-safe lazy-loading service container.

Definition:

type Cached[T any] struct {
    // Unexported fields
}

Features:


Functions

LazyLoad

Creates a lazy service loader for a named service.

Signature:

func LazyLoad[T any](serviceName string) *Cached[T]

Type Parameters:

Parameters:

Returns:

Example:

type UserService struct {
    db     *service.Cached[*DBPool]
    logger *service.Cached[*Logger]
}

func NewUserService() *UserService {
    return &UserService{
        db:     service.LazyLoad[*DBPool]("db-pool"),
        logger: service.LazyLoad[*Logger]("logger"),
    }
}

func (s *UserService) CreateUser(user *User) error {
    // Services loaded on first access
    db := s.db.Get()
    logger := s.logger.Get()
    
    logger.Info("Creating user")
    return db.Insert(user)
}

Use Cases:


LazyLoadWith

Creates a lazy loader with custom loader function.

Signature:

func LazyLoadWith[T any](loader func() T) *Cached[T]

Type Parameters:

Parameters:

Returns:

Example:

// Custom loader
dbLoader := service.LazyLoadWith(func() *DBPool {
    return connectToDB(os.Getenv("DB_URL"))
})

// In factory function
func MyServiceFactory(deps map[string]any, config map[string]any) any {
    return &MyService{
        db: service.LazyLoadWith(func() *DBPool {
            return deps["db"].(*DBPool)
        }),
    }
}

LazyLoadFrom

Creates a lazy loader from a ServiceGetter interface.

Signature:

func LazyLoadFrom[T any](getter ServiceGetter, serviceName string) *Cached[T]

Parameters:

Returns:

Example:

// Load from deployment app
app := deploy.GetServerApp("api", "crud-api")
userService := service.LazyLoadFrom[*UserService](app, "user-service")

// Service loaded on first access
users := userService.MustGet().GetAll()

Use Cases:


LazyLoadFromConfig

Creates a lazy loader from factory configuration.

Signature:

func LazyLoadFromConfig[T any](cfg map[string]any, key string) *Cached[T]

Parameters:

Returns:

Example:

// In factory function
func UserServiceFactory(deps map[string]any, config map[string]any) any {
    return &UserService{
        db:    service.LazyLoadFromConfig[*DBPool](config, "db"),
        cache: service.LazyLoadFromConfig[*Cache](config, "cache"),
    }
}

// Config YAML:
// service-definitions:
//   user-service:
//     type: user-service-factory
//     config:
//       db: db-pool
//       cache: redis-cache

MustLazyLoadFromConfig

Like LazyLoadFromConfig but panics if key is missing.

Signature:

func MustLazyLoadFromConfig[T any](cfg map[string]any, key string) *Cached[T]

Example:

func UserServiceFactory(deps map[string]any, config map[string]any) any {
    return &UserService{
        // Panic if "db" not in config (required dependency)
        db: service.MustLazyLoadFromConfig[*DBPool](config, "db"),
    }
}

Value

Creates a Cached instance with a pre-loaded value (no lazy loading).

Signature:

func Value[T any](value T) *Cached[T]

Parameters:

Returns:

Example:

// For testing
func TestUserService(t *testing.T) {
    mockDB := &MockDBPool{}
    mockLogger := &MockLogger{}
    
    svc := &UserService{
        db:     service.Value(mockDB),
        logger: service.Value(mockLogger),
    }
    
    // Services already loaded, no registry needed
    svc.CreateUser(&User{Name: "Test"})
}

Use Cases:


Cast

Converts a dependency value from map[string]any to typed Cached[T].

Signature:

func Cast[T any](value any) *Cached[T]

Parameters:

Returns:

Example:

func UserServiceFactory(deps map[string]any, config map[string]any) any {
    return &UserService{
        DB:     service.Cast[*DBPool](deps["db"]),
        Cache:  service.Cast[*Cache](deps["cache"]),
        Logger: service.Cast[*Logger](deps["logger"]),
    }
}

Use Cases:


CastProxyService

Casts a dependency value to *proxy.Service.

Signature:

func CastProxyService(value any) *proxy.Service

Parameters:

Returns:

Example:

// Remote service factory
func UserServiceRemoteFactory(deps map[string]any, config map[string]any) any {
    return &UserServiceRemote{
        proxyService: service.CastProxyService(deps["remote"]),
    }
}

Use Cases:


Cached[T] Methods

Get

Retrieves the service instance (loads on first call, cached thereafter).

Signature:

func (c *Cached[T]) Get() T

Returns:

Example:

type UserService struct {
    db *service.Cached[*DBPool]
}

func (s *UserService) GetUsers() []User {
    // Load DB on first call, use cached instance on subsequent calls
    db := s.db.Get()
    return db.QueryAll("SELECT * FROM users")
}

Thread-Safety:


MustGet

Retrieves the service instance or panics if not found.

Signature:

func (c *Cached[T]) MustGet() T

Returns:

Panics:

Example:

func (s *UserService) GetUsers() []User {
    // Panic if DB service not found (fail-fast)
    db := s.db.MustGet()
    return db.QueryAll("SELECT * FROM users")
}

When to Use:


ServiceName

Returns the service name being loaded.

Signature:

func (c *Cached[T]) ServiceName() string

Returns:

Example:

db := service.LazyLoad[*DBPool]("db-pool")
fmt.Println(db.ServiceName()) // Output: db-pool

IsLoaded

Checks if the service has been loaded.

Signature:

func (c *Cached[T]) IsLoaded() bool

Returns:

Example:

if !s.db.IsLoaded() {
    log.Println("DB not yet accessed")
}

Use Cases:


Complete Examples

Service with Dependencies

package service

import "github.com/primadi/lokstra/core/service"

type UserService struct {
    db         *service.Cached[*DBPool]
    cache      *service.Cached[*Cache]
    logger     *service.Cached[*Logger]
    mailSender *service.Cached[*MailSender]
}

func NewUserService() *UserService {
    return &UserService{
        db:         service.LazyLoad[*DBPool]("db-pool"),
        cache:      service.LazyLoad[*Cache]("redis-cache"),
        logger:     service.LazyLoad[*Logger]("logger"),
        mailSender: service.LazyLoad[*MailSender]("mail-sender"),
    }
}

func (s *UserService) CreateUser(user *User) error {
    // Services loaded lazily on first access
    db := s.db.Get()
    cache := s.cache.Get()
    logger := s.logger.Get()
    
    logger.Info("Creating user", user.Email)
    
    if err := db.Insert("users", user); err != nil {
        logger.Error("Failed to create user", err)
        return err
    }
    
    // Invalidate cache
    cache.Delete("users:list")
    
    // Send welcome email (async)
    go s.sendWelcomeEmail(user)
    
    return nil
}

func (s *UserService) sendWelcomeEmail(user *User) {
    mailer := s.mailSender.Get()
    mailer.Send(user.Email, "Welcome!", "Welcome to our platform!")
}

Factory Function with Dependencies

func UserServiceFactory(deps map[string]any, config map[string]any) any {
    return &UserService{
        db:         service.Cast[*DBPool](deps["db"]),
        cache:      service.Cast[*Cache](deps["cache"]),
        logger:     service.Cast[*Logger](deps["logger"]),
        mailSender: service.Cast[*MailSender](deps["mail"]),
    }
}

// Register factory
lokstra_registry.RegisterServiceType(
    "user-service-factory",
    UserServiceFactory,
    nil,
    deploy.WithDependencies("db", "cache", "logger", "mail"),
)

Optional Dependencies

type UserService struct {
    db     *service.Cached[*DBPool]
    cache  *service.Cached[*Cache]  // Optional
    logger *service.Cached[*Logger] // Optional
}

func (s *UserService) GetUser(id int) (*User, error) {
    db := s.db.MustGet() // Required
    
    // Check optional cache
    if s.cache != nil && s.cache.IsLoaded() {
        if user := s.cache.Get().Get("user:" + id); user != nil {
            return user.(*User), nil
        }
    }
    
    user, err := db.QueryOne("SELECT * FROM users WHERE id = ?", id)
    if err != nil {
        // Log if logger available
        if s.logger != nil {
            s.logger.Get().Error("DB query failed", err)
        }
        return nil, err
    }
    
    // Cache if available
    if s.cache != nil {
        s.cache.Get().Set("user:"+id, user, 5*time.Minute)
    }
    
    return user, nil
}

Testing with Mocks

func TestUserService_CreateUser(t *testing.T) {
    // Mock dependencies
    mockDB := &MockDBPool{}
    mockCache := &MockCache{}
    mockLogger := &MockLogger{}
    
    // Create service with pre-loaded mocks
    svc := &UserService{
        db:     service.Value(mockDB),
        cache:  service.Value(mockCache),
        logger: service.Value(mockLogger),
    }
    
    // Test
    user := &User{Name: "John", Email: "john@example.com"}
    err := svc.CreateUser(user)
    
    assert.NoError(t, err)
    assert.True(t, mockDB.InsertCalled)
    assert.True(t, mockCache.DeleteCalled)
    assert.True(t, mockLogger.InfoCalled)
}

Remote Service Implementation

type UserServiceRemote struct {
    proxyService *proxy.Service
}

func NewUserServiceRemote(proxyService *proxy.Service) *UserServiceRemote {
    return &UserServiceRemote{
        proxyService: proxyService,
    }
}

func (s *UserServiceRemote) GetUser(id int) (*User, error) {
    return proxy.CallWithData[*User](s.proxyService, "GetUser", id)
}

func (s *UserServiceRemote) CreateUser(user *User) (*User, error) {
    return proxy.CallWithData[*User](s.proxyService, "CreateUser", user)
}

// Factory
func UserServiceRemoteFactory(deps map[string]any, config map[string]any) any {
    return NewUserServiceRemote(
        service.CastProxyService(deps["remote"]),
    )
}

Best Practices

1. Use LazyLoad for Service Dependencies

// ✅ Good: Lazy loading
type UserService struct {
    db *service.Cached[*DBPool]
}

// 🚫 Avoid: Direct references (breaks lazy initialization)
type UserService struct {
    db *DBPool
}

2. Use MustGet for Required Dependencies

// ✅ Good: Fail-fast if missing
func (s *UserService) CreateUser(user *User) error {
    db := s.db.MustGet()
    return db.Insert(user)
}

// 🚫 Avoid: Silent failures
func (s *UserService) CreateUser(user *User) error {
    db := s.db.Get()
    if db == nil {
        return errors.New("db not available")
    }
    return db.Insert(user)
}

3. Use Value for Testing

// ✅ Good: Test with mocks
func TestService(t *testing.T) {
    mockDB := &MockDB{}
    svc := &UserService{
        db: service.Value(mockDB),
    }
    // ...
}

// 🚫 Avoid: Registry in tests
func TestService(t *testing.T) {
    lokstra_registry.RegisterService("db", mockDB)
    svc := NewUserService()
    // ...cleanup registry...
}

See Also