62 Resolvers with Dependency Injection in Go
62 Resolvers with Dependency Injection in Go
Dependency Injection (DI) in Go is an essential practice that supports testing, scalability, and separation of concerns. However, integrating DI techniques with dependency providers—such as resolvers in GraphQL—is still often confusing, especially for Go engineers used to a monolithic code structure. In this article, I’ll cover how to apply 62 resolvers with Dependency Injection in Go, break down the concept, explain best practices, and round it out with code examples, diagrams, and simulation tables. Let’s get started.
What Is a Resolver in the Context of Go?
In general, a resolver refers to a function/factory that “resolves” runtime dependencies while the code is running. The clearest real-world example shows up in GraphQL applications in Go, for instance using gqlgen . A resolver represents the layer between a GraphQL query and the business logic implementation, often depending on services, repositories, or other components.
The Challenge of Managing 62 Resolvers Manually
The main problems when managing many resolvers in Go:
- Boilerplate code duplication: Injecting dependencies manually into every resolver.
- Hard to maintain: Each time you add a new dependency, you have to change the constructor of every resolver.
- Harder to test: If DI isn’t consistent, mocking and testing become complicated.
Basic Dependency Injection in Go
Go isn’t Java or .NET, which have a native DI container. Even so, the dependency injection practice can still be applied using the principles of constructor injection or interface injection.
Here’s the most basic pattern:
1type UserService interface {
2 GetUser(id string) (*User, error)
3}
4
5type UserResolver struct {
6 Service UserService
7}
8
9func NewUserResolver(svc UserService) *UserResolver {
10 return &UserResolver{Service: svc}
11}For 62 resolvers, without proper DI:
1var postResolver = NewPostResolver(postService)
2var commentResolver = NewCommentResolver(commentService)
3var friendResolver = NewFriendResolver(friendService)
4// and so on for all 62 resolvers...This is too verbose and error-prone.
Building an Effective Dependency Injection Layer
Popular strategies for resolver injection in Go:
- Manual wiring — Directly wiring up dependencies in main.go or the composition root
- Using a DI container — Third-party libraries such as
uber-go/dig
Let’s look at both.
1. Manual Wiring
The simplest and most “Go idiomatic” approach. The following table compares manual wiring and a container:
| Criteria | Manual Wiring | DI Container (dig) |
|---|---|---|
| Boilerplate | Quite a lot | Minimal |
| Compile-time Safety | High | Medium |
| Easy Refactoring | Depends | Easy |
| Learning Curve | Low | Medium |
Manual Wiring Example (Constructor Injection)
Suppose we have 3 services and want to extend to 62 resolvers (the code below shows the basic principle):
1type AppResolvers struct {
2 UserResolver *UserResolver
3 PostResolver *PostResolver
4 CommentResolver *CommentResolver
5 // ... up to 62 resolvers
6}
7
8func NewAppResolvers(db *sql.DB) *AppResolvers {
9 userService := NewUserService(db)
10 postService := NewPostService(db)
11 commentService := NewCommentService(db)
12
13 return &AppResolvers{
14 UserResolver: NewUserResolver(userService),
15 PostResolver: NewPostResolver(postService),
16 CommentResolver: NewCommentResolver(commentService),
17 }
18}If the dependency chain gets deeper (e.g., a service depends on a repository, which depends on a cache, and so on), the wiring grows longer and longer.
2. Dependency Injection Container with Uber-dig
If the number of resolvers reaches the dozens, a library like Uber Dig can be very helpful. dig simplifies building the dependency graph and resolves dependencies automatically.
Installation
1go get go.uber.org/digImplementation with Uber-dig: Resolver Simulation
Suppose we have 3 resolvers out of a total of 62 (for brevity):
1import (
2 "database/sql"
3 "go.uber.org/dig"
4)
5
6type UserService struct{ db *sql.DB }
7type PostService struct{ db *sql.DB }
8type CommentService struct{ db *sql.DB }
9
10func NewUserService(db *sql.DB) *UserService { return &UserService{db} }
11func NewPostService(db *sql.DB) *PostService { return &PostService{db} }
12func NewCommentService(db *sql.DB) *CommentService{ return &CommentService{db} }
13
14type UserResolver struct{ Service *UserService }
15type PostResolver struct{ Service *PostService }
16type CommentResolver struct{ Service *CommentService }
17
18func NewUserResolver(svc *UserService) *UserResolver { return &UserResolver{svc} }
19func NewPostResolver(svc *PostService) *PostResolver { return &PostResolver{svc} }
20func NewCommentResolver(svc *CommentService) *CommentResolver { return &CommentResolver{svc} }
21
22func main() {
23 container := dig.New()
24 container.Provide(sql.Open) // supply *sql.DB
25 container.Provide(NewUserService)
26 container.Provide(NewPostService)
27 container.Provide(NewCommentService)
28 container.Provide(NewUserResolver)
29 container.Provide(NewPostResolver)
30 container.Provide(NewCommentResolver)
31
32 // resolve all resolvers
33 container.Invoke(func(
34 userResolver *UserResolver,
35 postResolver *PostResolver,
36 commentResolver *CommentResolver,
37 ) {
38 // can be injected into a router, GraphQL server, etc.
39 })
40}Scalability: Just add a .Provide() for each new resolver/service. Uber-dig untangles the dependency graph and injects everything automatically, even when there are hundreds of them.
Dependency Injection Flow Diagram
Let’s look at an overview of resolver wiring with DI using mermaid:
graph TD
subgraph Database Layer
DB[(Database)]
end
subgraph Service Layer
USV[UserService]
PSV[PostService]
CSV[CommentService]
end
subgraph Resolver Layer
UR[UserResolver]
PR[PostResolver]
CR[CommentResolver]
end
DB --> USV
DB --> PSV
DB --> CSV
USV --> UR
PSV --> PR
CSV --> CR
%% And so on, up to 62 resolvers
Imagine the diagram above growing to 62 nodes in the Service & Resolver layers—without a DI container, the wiring is extremely prone to errors.
Simulation Study: Adding the 63rd Resolver
Without a DI Container
- Modify the
AppResolversconstructor - Add the dependency in main.go
- Add/match it in each service and resolver
- High risk of error
With a DI Container
- Add
.Provide(NewNewResolver)to the container - The implementation stays consistent
- No need to modify existing code
Simulation Table for Adding a Dependency:
| Dependency | Manual Wiring (Steps) | uber-dig (Steps) |
|---|---|---|
| Code in the Resolver | 1 | 1 |
| Code in AppResolvers | 1 | 0 |
| Code in main.go | 2-3 | 1 |
| Human Error Risk | Medium | Low |
Testing Resolvers with Dependency Injection
The main benefit of DI: easier testing! We can mock services, inject dependencies into resolvers, and isolate unit tests.
1func TestUserResolver_GetUser(t *testing.T) {
2 mockService := &MockUserService{}
3 resolver := NewUserResolver(mockService)
4 // continue with the test
5}Conclusion
Managing 62+ resolvers in a Go project is a serious challenge if dependencies aren’t designed well. Dependency Injection—whether manual or using a container like Uber-dig—is a scalable, testable, and maintainable solution. Even though Go doesn’t have a built-in DI container, the constructor injection pattern and adopting an external library provide great flexibility for handling complex codebases.
Make DI your new standard when building large-scale Go projects—whether for GraphQL resolvers, REST handlers, or any other service logic!
References:
Hopefully this article helps you build scalable resolvers in Go!
Have fun coding 🚀