A lightweight, reflection-based dependency injection container for Go.
- 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 implementingdepot.Closerin 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
go get github.com/zignd/depotpackage 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
}// 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 = 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.
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 factorydepot.SingletonNamed(name, factory)- Register a named singleton factorydepot.Transient(factory)- Register a transient factorydepot.TransientNamed(name, factory)- Register a named transient factorydepot.Instance(instance)- Register a pre-created instancedepot.InstanceNamed(name, instance)- Register a named pre-created instance
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
})Same as RegisterSingleton, but registered under a specific name.
err := dp.RegisterSingletonByName("createOrderUC", func(db *DBConnection) *CreateOrderUseCase {
return &CreateOrderUseCase{DB: db}
})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()}
})Same as RegisterTransient, but registered under a specific name.
err := dp.RegisterTransientByName("sessionKey", func() *Key {
return &Key{Value: generateKey()}
})Registers an existing instance that will be returned whenever the type is resolved.
orderService := &OrderService{/* ... */}
err := dp.RegisterInstance(orderService)Same as RegisterInstance, but registered under a specific name.
err := dp.RegisterInstanceByName("mainLogger", &logger)Type-safe resolution using Go generics by type.
dbConn, err := depot.Get[*DBConnection](dp)Type-safe resolution using Go generics by name.
createOrderUC, err := depot.GetByName[*CreateOrderUseCase](dp, "createOrderUC")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 checkingType-safe named resolution that panics on error.
createOrderUC := depot.MustGetByName[*CreateOrderUseCase](dp, "createOrderUC")
// Use createOrderUC directly without error checkingResolves a dependency by type and sets it to the provided pointer.
var dbConnection *DBConnection
err := dp.Resolve(&dbConnection)Resolves a dependency by name and sets it to the provided pointer.
var createOrderUC *CreateOrderUseCase
err := dp.ResolveByName("createOrderUC", &createOrderUC)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 checkingResolves 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 checkingGracefully 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()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)
}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 succeededThis 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.
See the following examples:
- example/main.go - Complete working example with all features
- example/register_many/main.go - Clean registration using
RegisterMany - example/must_methods/main.go - Using
Must*methods for cleaner code - example/lazy_vs_eager/main.go - Detailed demonstration of lazy vs eager initialization modes
- example/getbyname/main.go - Demonstration of type-safe named resolution with
GetByName - example/error_checking/main.go - Error tracking example (with intentional errors)
- example/error_checking_success/main.go - Batch error checking with successful registrations
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)
}
}- 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
- 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
- Pre-created instance is registered
- Same instance always returned
- Useful for: externally created objects, testing
- Already created - not affected by LazyMode setting
The LazyMode configuration option controls when singleton dependencies are created:
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 requestedAdvantages:
- 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
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 instanceAdvantages:
- 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
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
// 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 resolutionFactories 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
}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 failsMustGetByName[T]- Panics if named resolution failsMustResolve- Panics if resolution by pointer failsMustResolveByName- 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
- Use pointer types for singletons - This ensures the same instance is shared across all consumers
- Choose the right mode - Use eager mode for production (fail-fast), lazy mode for development/conditional dependencies
- Register dependencies in order (eager mode) - In eager mode, ensure dependencies are registered before their consumers
- Handle errors - Always check for errors when resolving dependencies (and registering in eager mode)
- Call Shutdown - Ensure proper cleanup by calling
dp.Shutdown()before your application exits - Use named registrations sparingly - Only when you need multiple registrations of the same type
- Validate at startup (eager mode) - Take advantage of eager mode's fail-fast behavior in production to catch issues early
MIT