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 &registry{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:

  1. Create pkg/registry/order.go with 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)
}
  1. Extend AppController in pkg/adapter/controller/app.go:
type AppController struct {
    User  controller.User
    Order controller.Order  // new field
}
  1. Update NewAppController in pkg/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.go and 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 AppController aggregation, 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.go wire together repositories, use cases, and controllers while maintaining interface-based dependencies
  • The main.go entry point initializes the registry once and extracts the fully configured AppController for 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →