Skip to content

zignd/depot

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

depot GoDoc Report card

A lightweight, reflection-based dependency injection container for Go.

Features

  • Singleton, Transient, and Instance lifetimes - Full control over object lifecycle
  • Lazy vs Eager initialization - Choose between lazy (on-demand) or eager (immediate) singleton creation
  • Automatic dependency resolution - Dependencies are automatically injected based on factory function parameters
  • Generic type-safe resolution - Use Get[T]() for compile-time type safety
  • Named registrations - Register and resolve dependencies by name when type alone isn't sufficient
  • Graceful shutdown - Automatically calls Close() on all instances implementing depot.Closer in reverse dependency order
  • Factory functions with error handling - Factories can return (T, error) for initialization that can fail
  • Fail-fast error detection - In eager mode, initialization errors are caught immediately during registration

Installation

go get github.com/zignd/depot

Quick Start

package main

import (
    "github.com/zignd/depot"
)

type Config struct {
    DatabaseURL string
}

type Logger struct {
    Prefix string
}

type DBConnection struct {
    URL    string
    Logger *Logger
}

func (db *DBConnection) Close() error {
    // cleanup
    return nil
}

func main() {
    dp := depot.New(depot.Config{LazyMode: true})
    
    // Register factories
    dp.RegisterSingleton(func() Config {
        return Config{DatabaseURL: "postgres://localhost/mydb"}
    })
    
    dp.RegisterSingleton(func() *Logger {
        return &Logger{Prefix: "[APP]"}
    })
    
    // Dependencies are automatically resolved
    dp.RegisterSingleton(func(c Config, l *Logger) (*DBConnection, error) {
        return &DBConnection{URL: c.DatabaseURL, Logger: l}, nil
    })
    
    // Resolve with type safety using Get
    db, err := depot.Get[*DBConnection](dp)
    
    // Or register and resolve by name
    dp.RegisterSingletonByName("myDB", func(c Config, l *Logger) (*DBConnection, error) {
        return &DBConnection{URL: c.DatabaseURL, Logger: l}, nil
    })
    namedDB, err := depot.GetByName[*DBConnection](dp, "myDB")
    
    // Cleanup
    dp.Shutdown() // Calls Close() on DBConnection
}

API Reference

Creating a Depot

// Lazy mode (default: false) - singletons created on first request
dp := depot.New(depot.Config{
    LazyMode: true,
})

// Eager mode - singletons created immediately upon registration
dp := depot.New(depot.Config{
    LazyMode: false,
})

LazyMode Explained

LazyMode = true (Lazy Initialization)

  • Singletons are created only when first requested
  • Faster startup time as dependencies are created on-demand
  • Initialization errors occur during resolution
  • Good for applications with many dependencies where not all are always needed
  • Better for development and testing

LazyMode = false (Eager Initialization)

  • Singletons are created immediately when registered
  • Slower startup but faster subsequent access
  • Initialization errors are caught immediately during registration (fail-fast)
  • All dependencies are validated at startup
  • Better for production environments where you want to catch configuration errors early

Note: Transient and Instance lifetimes are not affected by LazyMode. Transients are always created on-demand, and instances are already created.

Registration

RegisterMany

Register multiple dependencies in a single call using registration helper functions. This is the cleanest way to register many dependencies at once.

err := dp.RegisterMany(
    depot.Singleton(func() Config {
        return Config{DatabaseURL: "postgres://localhost/mydb"}
    }),
    depot.Singleton(func(c Config) *Logger {
        return &Logger{Prefix: "[APP]"}
    }),
    depot.Singleton(func(c Config, l *Logger) (*Database, error) {
        return &Database{Config: c, Logger: l}, nil
    }),
    depot.Transient(func() *SessionToken {
        return GenerateNewToken()
    }),
    depot.Instance(&myService),
)

// Check all registration errors at once
if errs := dp.Errors(); errs != nil {
    for _, err := range errs {
        log.Printf("Registration error: %v", err)
    }
    return
}

Helper functions available for RegisterMany:

  • depot.Singleton(factory) - Register a singleton factory
  • depot.SingletonNamed(name, factory) - Register a named singleton factory
  • depot.Transient(factory) - Register a transient factory
  • depot.TransientNamed(name, factory) - Register a named transient factory
  • depot.Instance(instance) - Register a pre-created instance
  • depot.InstanceNamed(name, instance) - Register a named pre-created instance

RegisterSingleton

Registers a factory that will be called once, and the same instance will be returned for all subsequent resolutions.

err := dp.RegisterSingleton(func() Config {
    return Config{Value: "config"}
})

// With dependencies
err := dp.RegisterSingleton(func(c Config, l *Logger) (*DBConnection, error) {
    return &DBConnection{Config: c, Logger: l}, nil
})

RegisterSingletonByName

Same as RegisterSingleton, but registered under a specific name.

err := dp.RegisterSingletonByName("createOrderUC", func(db *DBConnection) *CreateOrderUseCase {
    return &CreateOrderUseCase{DB: db}
})

RegisterTransient

Registers a factory that will be called every time the dependency is resolved, creating a new instance each time.

err := dp.RegisterTransient(func() *Locker {
    return &Locker{ID: generateID()}
})

RegisterTransientByName

Same as RegisterTransient, but registered under a specific name.

err := dp.RegisterTransientByName("sessionKey", func() *Key {
    return &Key{Value: generateKey()}
})

RegisterInstance

Registers an existing instance that will be returned whenever the type is resolved.

orderService := &OrderService{/* ... */}
err := dp.RegisterInstance(orderService)

RegisterInstanceByName

Same as RegisterInstance, but registered under a specific name.

err := dp.RegisterInstanceByName("mainLogger", &logger)

Resolution

Get (Generic)

Type-safe resolution using Go generics by type.

dbConn, err := depot.Get[*DBConnection](dp)

GetByName (Generic)

Type-safe resolution using Go generics by name.

createOrderUC, err := depot.GetByName[*CreateOrderUseCase](dp, "createOrderUC")

MustGet (Generic)

Type-safe resolution that panics on error. Useful when you're certain the dependency exists and want cleaner code without error handling.

db := depot.MustGet[*Database](dp)
// Use db directly without error checking

MustGetByName (Generic)

Type-safe named resolution that panics on error.

createOrderUC := depot.MustGetByName[*CreateOrderUseCase](dp, "createOrderUC")
// Use createOrderUC directly without error checking

Resolve

Resolves a dependency by type and sets it to the provided pointer.

var dbConnection *DBConnection
err := dp.Resolve(&dbConnection)

ResolveByName

Resolves a dependency by name and sets it to the provided pointer.

var createOrderUC *CreateOrderUseCase
err := dp.ResolveByName("createOrderUC", &createOrderUC)

MustResolve

Resolves a dependency by type and sets it to the provided pointer. Panics on error.

var db *Database
dp.MustResolve(&db)
// Use db directly without error checking

MustResolveByName

Resolves a dependency by name and sets it to the provided pointer. Panics on error.

var createOrderUC *CreateOrderUseCase
dp.MustResolveByName("createOrderUC", &createOrderUC)
// Use createOrderUC directly without error checking

Shutdown

Gracefully shuts down all created instances by calling Close() on those implementing depot.Closer, in reverse order of creation (from least dependent to most dependent).

type Closer interface {
    Close() error
}

err := dp.Shutdown()

Error Checking

Individual Error Checking (Traditional Approach)

You can check for errors immediately after each registration:

if err := dp.RegisterSingleton(factory1); err != nil {
    log.Fatal(err)
}
if err := dp.RegisterSingleton(factory2); err != nil {
    log.Fatal(err)
}

Batch Error Checking

Alternatively, you can register all dependencies first and check for errors in one place using Errors():

// Register all dependencies without checking errors individually
dp.RegisterSingleton(factory1)
dp.RegisterSingleton(factory2)
dp.RegisterTransient(factory3)
dp.RegisterInstance(instance1)

// Check all registration errors at once
if errs := dp.Errors(); errs != nil {
    for _, err := range errs {
        log.Printf("Registration error: %v", err)
    }
    return
}

// If we get here, all registrations succeeded

This approach is cleaner when registering many dependencies and allows you to collect all errors before deciding how to handle them.

Note: Errors are tracked internally and are not cleared after calling Errors(). Both approaches can be used together if needed.

Complete Example

See the following examples:

package main

import (
    "fmt"
    "log"
    "github.com/zignd/depot"
)

type Config struct {
    DatabaseURL string
}

type Logger struct {
    Prefix string
}

func (l *Logger) Close() error {
    fmt.Println("Logger closed")
    return nil
}

type DBConnection struct {
    URL    string
    Logger *Logger
}

func (db *DBConnection) Close() error {
    db.Logger.Log("Closing database connection")
    return nil
}

func main() {
    dp := depot.New(depot.Config{LazyMode: true})
    
    // Register all dependencies
    dp.RegisterSingleton(func() Config {
        return Config{DatabaseURL: "postgres://localhost:5432/mydb"}
    })
    
    dp.RegisterSingleton(func() *Logger {
        return &Logger{Prefix: "[APP]"}
    })
    
    dp.RegisterSingleton(func(c Config, l *Logger) (*DBConnection, error) {
        return &DBConnection{URL: c.DatabaseURL, Logger: l}, nil
    })
    
    // Resolve
    db, err := depot.Get[*DBConnection](dp)
    if err != nil {
        log.Fatal(err)
    }
    
    // Use db...
    
    // Cleanup
    if err := dp.Shutdown(); err != nil {
        log.Fatal(err)
    }
}

Lifetimes

Singleton

  • Factory is called once
  • Same instance returned for all resolutions
  • Useful for: database connections, configuration, loggers
  • In Lazy mode: Created on first resolution
  • In Eager mode: Created immediately upon registration

Transient

  • Factory is called every time the dependency is resolved
  • New instance created each time
  • Useful for: temporary objects, stateful operations
  • Always lazy - not affected by LazyMode setting

Instance

  • Pre-created instance is registered
  • Same instance always returned
  • Useful for: externally created objects, testing
  • Already created - not affected by LazyMode setting

Lazy vs Eager Initialization

The LazyMode configuration option controls when singleton dependencies are created:

Lazy Mode (LazyMode: true)

dp := depot.New(depot.Config{LazyMode: true})

var initOrder []string

dp.RegisterSingleton(func() Config {
    initOrder = append(initOrder, "Config")
    return Config{}
})

dp.RegisterSingleton(func(c Config) *Logger {
    initOrder = append(initOrder, "Logger")
    return &Logger{}
})

// At this point, initOrder is still empty []
// Singletons are NOT created yet

logger, _ := depot.Get[*Logger](dp)

// Now initOrder is ["Config", "Logger"]
// Both were created on-demand when Logger was requested

Advantages:

  • Faster application startup
  • Only creates dependencies that are actually used
  • Useful in development/testing when you may not need all dependencies
  • Good for applications with conditional dependency usage

Disadvantages:

  • First request is slower as it triggers initialization
  • Initialization errors happen during runtime, not at startup
  • Harder to detect configuration issues early

Eager Mode (LazyMode: false)

dp := depot.New(depot.Config{LazyMode: false})

var initOrder []string

dp.RegisterSingleton(func() Config {
    initOrder = append(initOrder, "Config")
    return Config{}
})
// initOrder is now ["Config"] - created immediately!

dp.RegisterSingleton(func(c Config) *Logger {
    initOrder = append(initOrder, "Logger")
    return &Logger{}
})
// initOrder is now ["Config", "Logger"] - Logger created immediately!

logger, _ := depot.Get[*Logger](dp)
// Logger was already created, just returns existing instance

Advantages:

  • Fail-fast: catches initialization errors at registration time
  • All dependencies validated at startup
  • Consistent, predictable performance (no lazy initialization overhead)
  • Better for production environments

Disadvantages:

  • Slower application startup
  • Creates all singletons even if some are never used
  • Requires all dependencies to be registered in correct order

Choosing Between Lazy and Eager

Use Lazy Mode when:

  • You have many dependencies but only use a subset in each execution path
  • Fast startup time is critical
  • You're in development/testing and want flexibility
  • Your application has conditional feature activation

Use Eager Mode when:

  • You want to catch configuration errors at startup
  • All dependencies are always used
  • Predictable performance is important
  • You're deploying to production and want fail-fast behavior

Example: Error Handling Difference

// Eager mode - error caught during registration
dp := depot.New(depot.Config{LazyMode: false})

err := dp.RegisterSingleton(func() (*DB, error) {
    return nil, errors.New("connection failed")
})
// err is "eager initialization failed: connection failed"
// Registration fails, depot remains in valid state

// Lazy mode - error delayed until resolution
dp := depot.New(depot.Config{LazyMode: true})

err := dp.RegisterSingleton(func() (*DB, error) {
    return nil, errors.New("connection failed")
})
// err is nil, registration succeeds

db, err := depot.Get[*DB](dp)
// NOW err is "connection failed"
// Error occurs during resolution

Error Handling

Factories can return an error as the second return value:

dp.RegisterSingleton(func(config Config) (*DBConnection, error) {
    if config.DatabaseURL == "" {
        return nil, errors.New("database URL is required")
    }
    return &DBConnection{URL: config.DatabaseURL}, nil
})

db, err := depot.Get[*DBConnection](dp)
if err != nil {
    // Handle initialization error
}

Must* Methods

For cases where you're certain a dependency exists and want cleaner code without error handling, use the Must* variants. These methods panic if resolution fails:

// Instead of:
db, err := depot.Get[*Database](dp)
if err != nil {
    log.Fatal(err)
}

// Use:
db := depot.MustGet[*Database](dp)

Available Must methods:*

  • MustGet[T] - Panics if type resolution fails
  • MustGetByName[T] - Panics if named resolution fails
  • MustResolve - Panics if resolution by pointer fails
  • MustResolveByName - Panics if named resolution by pointer fails

When to use Must methods:*

  • During application initialization when missing dependencies should be fatal
  • In main() or init() functions where you want fail-fast behavior
  • When you've already validated that all dependencies are registered
  • In tests where panics are acceptable

When NOT to use Must methods:*

  • In library code that others will consume
  • When you need to handle missing dependencies gracefully
  • In HTTP handlers or other request-scoped code
  • When optional dependencies are acceptable

Best Practices

  1. Use pointer types for singletons - This ensures the same instance is shared across all consumers
  2. Choose the right mode - Use eager mode for production (fail-fast), lazy mode for development/conditional dependencies
  3. Register dependencies in order (eager mode) - In eager mode, ensure dependencies are registered before their consumers
  4. Handle errors - Always check for errors when resolving dependencies (and registering in eager mode)
  5. Call Shutdown - Ensure proper cleanup by calling dp.Shutdown() before your application exits
  6. Use named registrations sparingly - Only when you need multiple registrations of the same type
  7. Validate at startup (eager mode) - Take advantage of eager mode's fail-fast behavior in production to catch issues early

License

MIT

About

A lightweight, reflection-based dependency injection container for Go.

Resources

License

Stars

Watchers

Forks

Packages

No packages published