Understanding the Dependency Injection Registry Pattern in Go: A Complete Guide
The dependency injection registry pattern in Go creates a centralized container that wires together clean architecture layers by holding a single database connection and exposing factory methods to instantiate controllers, use cases, and repositories.
This pattern is exemplified in the manakuro/golang-clean-architecture repository, where a lightweight DI container eliminates tight coupling between infrastructure and business logic. By isolating object creation in a dedicated registry package, the codebase achieves true separation of concerns while maintaining straightforward testability.
What Is the Dependency Injection Registry Pattern?
The dependency injection registry pattern implements a centralized object factory that manages the lifecycle and wiring of application components. Unlike service locators that are called dynamically throughout the code, a registry initializes dependencies once during application startup and injects them into the appropriate layers.
In Go implementations, this pattern typically involves:
- A registry struct that holds shared infrastructure dependencies (such as database connections)
- Factory methods that instantiate concrete implementations of interfaces
- Clean separation between the creation logic and business logic
How the Registry Pattern Works in golang-clean-architecture
The pkg/registry package in manakuro/golang-clean-architecture demonstrates a production-ready implementation that wires together four distinct layers: infrastructure, use case, adapter, and delivery.
The Core Registry Structure
At the heart of the pattern lies a minimal struct that captures only the runtime dependencies required by the entire application. In pkg/registry/registry.go, the implementation stores a single *gorm.DB instance:
type registry struct {
db *gorm.DB
}
func NewRegistry(db *gorm.DB) Registry {
return ®istry{db}
}
This design ensures that database connection configuration happens once at startup and propagates consistently through all downstream components.
Factory Methods for Clean Architecture Layers
The registry exposes methods that construct complete dependency chains for specific domains. The NewUserController method in pkg/registry/user.go illustrates how the pattern wires together multiple layers:
func (r *registry) NewUserController() controller.User {
u := usecase.NewUserUsecase(
repository.NewUserRepository(r.db),
repository.NewDBRepository(r.db),
)
return controller.NewUserController(u)
}
This factory method demonstrates dependency inversion: the controller depends on the use case interface, the use case depends on repository interfaces, but only the registry knows the concrete implementations.
Wiring Dependencies in Main
The application entry point in cmd/app/main.go orchestrates the initialization sequence by creating the registry and extracting the fully configured controller graph:
db := datastore.NewDB()
defer db.Close()
r := registry.NewRegistry(db)
e = router.NewRouter(echo.New(), r.NewAppController())
This approach keeps main.go free of complex wiring logic while ensuring that all components receive their dependencies before the HTTP server starts.
Practical Implementation Examples
Creating the DI Container in Production
When deploying the application, initialize the registry immediately after establishing infrastructure connections:
package main
import (
"github.com/manakuro/golang-clean-architecture/pkg/infrastructure/datastore"
"github.com/manakuro/golang-clean-architecture/pkg/registry"
"github.com/manakuro/golang-clean-architecture/pkg/infrastructure/router"
"github.com/labstack/echo/v4"
)
func main() {
config.ReadConfig()
db := datastore.NewDB()
defer db.Close()
registry := registry.NewRegistry(db)
appController := registry.NewAppController()
e := echo.New()
router.NewRouter(e, appController)
e.Start(":8080")
}
Mocking Dependencies for Testing
The registry pattern enables seamless testing by allowing you to substitute concrete implementations with mocks. Create a test registry that returns mock repositories instead of database-backed ones:
type mockUserRepo struct{}
func (m *mockUserRepo) FindByID(id uint) (*model.User, error) {
return &model.User{ID: id, Name: "Test"}, nil
}
type mockDBRepo struct{}
func (m *mockDBRepo) Transaction(txFunc func(interface{}) error) error {
return txFunc(nil)
}
func TestUserController(t *testing.T) {
// Manually construct the dependency chain with mocks
mockUser := &mockUserRepo{}
mockDB := &mockDBRepo{}
uc := usecase.NewUserUsecase(mockUser, mockDB)
ctrl := controller.NewUserController(uc)
// Execute tests against ctrl with deterministic behavior
user, err := ctrl.GetUser(1)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if user.Name != "Test" {
t.Errorf("expected name 'Test', got %s", user.Name)
}
}
Adding a New Service to the Registry
When extending the application with a new domain (for example, an Order service), follow the established pattern to maintain consistency:
- Create
pkg/registry/order.gowith a factory method:
package registry
import (
"github.com/manakuro/golang-clean-architecture/pkg/adapter/controller"
"github.com/manakuro/golang-clean-architecture/pkg/usecase/usecase"
"github.com/manakuro/golang-clean-architecture/pkg/usecase/repository"
)
func (r *registry) NewOrderController() controller.Order {
o := usecase.NewOrderUsecase(
repository.NewOrderRepository(r.db),
repository.NewDBRepository(r.db),
)
return controller.NewOrderController(o)
}
- Extend
AppControllerinpkg/adapter/controller/app.go:
type AppController struct {
User controller.User
Order controller.Order // new field
}
- Update
NewAppControllerinpkg/registry/registry.go:
func (r *registry) NewAppController() controller.AppController {
return controller.AppController{
User: r.NewUserController(),
Order: r.NewOrderController(), // wire the new controller
}
}
This approach ensures the new service integrates automatically without modifying existing business logic or infrastructure code.
Benefits of Using a Registry for Dependency Injection
Implementing the dependency injection registry pattern in Go delivers several architectural advantages that align with clean architecture principles:
-
Strict Decoupling – Business logic in use cases and delivery handlers depends only on interfaces defined in the domain layer. The registry is the sole location aware of concrete implementations, eliminating import cycles and layer violations.
-
Enhanced Testability – By substituting the registry's factory methods or manually constructing dependency chains with mocks, you can test controllers and use cases in isolation without database dependencies or external services.
-
Centralized Configuration – Database connections, cache clients, and other infrastructure resources are instantiated once in
main.goand passed to the registry, preventing connection leaks and configuration drift across the codebase. -
Scalable Service Addition – Adding new domains requires only creating a new factory method in the registry and updating the
AppControlleraggregation, leaving existing code untouched and reducing regression risk.
Summary
The dependency injection registry pattern in Go provides a lightweight, manual approach to wiring clean architecture components without heavy frameworks. Key takeaways include:
- The registry struct holds minimal runtime state (typically just a
*gorm.DB) and exposes factory methods to construct concrete implementations - Factory methods in files like
pkg/registry/user.gowire together repositories, use cases, and controllers while maintaining interface-based dependencies - The
main.goentry point initializes the registry once and extracts the fully configuredAppControllerfor the HTTP router - Testing becomes straightforward by either mocking the registry or manually injecting mock implementations into the dependency chain
- New services integrate cleanly by adding factory methods without modifying existing business logic
Frequently Asked Questions
What is the difference between a registry and a service locator?
A registry initializes dependencies during application startup and injects them into constructors, whereas a service locator is called dynamically at runtime to look up dependencies. The registry pattern in manakuro/golang-clean-architecture promotes explicit dependency injection through factory methods, making dependencies visible in constructors and easier to trace than the hidden dependencies typical of service locators.
How does the registry pattern improve testability in Go?
The registry pattern improves testability by centralizing concrete implementation choices in factory methods. During testing, you can replace these factory methods with versions that return mocks, or bypass the registry entirely to inject mock repositories and use cases directly into controllers. This approach allows unit tests to run without database connections or external services, as demonstrated in the TestUserController example that uses mockUserRepo and mockDBRepo.
Can I use the registry pattern with interfaces other than GORM?
Yes, the registry pattern is database-agnostic. While manakuro/golang-clean-architecture uses *gorm.DB as the infrastructure dependency, you can adapt the pattern to hold any shared resource such as *sql.DB, Redis clients, MongoDB sessions, or cloud service clients. The key principle is that the registry holds concrete infrastructure instances and wires them into repository implementations that satisfy domain interfaces.
Where should I initialize the registry in a Go application?
Initialize the registry in the main.go entry point immediately after establishing infrastructure connections. In manakuro/golang-clean-architecture, this occurs in cmd/app/main.go where the database connection is created, passed to registry.NewRegistry(), and the resulting AppController is handed to the router. This ensures all dependencies are wired before the HTTP server starts accepting requests and allows proper resource cleanup via defer db.Close().
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →