Wiring (Dependency Injection)¶
The backend uses Google Wire for compile-time dependency injection. Every constructor (NewXxx) is grouped into a provider set; Wire generates wire_gen.go from those sets, producing a single InitializeApp function that builds the entire object graph in the correct order.
Source files:
backend/internal/wiring/providers.go— all provider sets and helper providersbackend/cmd/server/wire.go— thewire.Build(AppSet)directive (build-tagwireinject)backend/cmd/server/wire_gen.go— generated, do not edit
Why Wire¶
- Compile-time checks. Missing dependencies are caught by
go build, not at startup. - Zero runtime cost. The output is just
New*calls in topological order. - Explicit object graph. Reading
wire_gen.gotells you exactly how the app is assembled — no reflection magic.
Provider sets¶
providers.go defines seven coarse-grained sets and one top-level AppSet that aggregates them:
| Set | What it provides |
|---|---|
ConfigSet |
Extracts per-domain config structs from *config.Config (ProvideJWTConfig, ProvideGoogleConfig, ProvideStripeConfig, etc.) and a few scalars (ProvideFrontendURL, ProvideEncryptionKey, ProvideWorkerConcurrency) |
InfrastructureSet |
The *gorm.DB, the shared *redis.Client (also bound as redis.Cmdable), and the storage.FileStorage implementation |
RepositorySet |
All repositories (UserRepository, EventRepository, FriendRepository, …) and the wire.Binds that satisfy service-level interfaces |
ServiceSet |
All services (AuthService, UserService, EventService, ChatService, RedisService, EmailService, etc.) plus the WebSocket Hub and the webhook service |
HandlerSet |
All HTTP handlers and the Handlers aggregate struct |
WorkerSet |
The Asynq Client, Worker, and TaskHandler |
SchedulerSet |
The distributed scheduler Manager |
AppSet combines all of the above plus ProvideRateLimiter, ProvideIPBlocker, and NewApp:
var AppSet = wire.NewSet(
ConfigSet, InfrastructureSet, RepositorySet, ServiceSet,
HandlerSet, WorkerSet, SchedulerSet,
ProvideRateLimiter, ProvideIPBlocker, NewApp,
)
Regenerating wire_gen.go¶
After any change to providers (new constructor, new dependency on an existing constructor, new wire.Bind):
task gen:wire
# or, from backend/:
wire ./cmd/server
If Wire is not installed:
go install github.com/google/wire/cmd/wire@latest
Commit wire_gen.go alongside the provider change so CI builds match local builds.
Don't edit wire_gen.go by hand
The file is regenerated end-to-end by Wire. Manual edits will be silently overwritten the next time wire runs.
Adding a new dependency¶
The common path:
- Write the constructor. E.g.
services.NewSearchService(db *gorm.DB, redis services.RedisService) *services.SearchService. Constructor parameters define what Wire injects. - Add it to the appropriate set. Open
providers.goand append toServiceSet(orRepositorySet, etc.):
var ServiceSet = wire.NewSet(
// ...
services.NewSearchService,
)
- Bind to an interface, if needed. If a downstream consumer takes an interface rather than a concrete type, add a
wire.Bind:
wire.Bind(new(handlers.SearchEngine), new(*services.SearchService)),
-
Wire it into an aggregate. If the new dependency is used by
Appdirectly (rare), add it toApp/NewApp. If it's used by a handler, add it to that handler's constructor and to theHandlersaggregate. -
Regenerate:
task gen:wire. -
Build:
task build:backend. Any cycle, missing provider, or duplicate binding fails the build with a clear error.
Common gotchas¶
- Interface vs concrete. Wire injects whatever type the parameter declares. If a service takes
services.RedisService(interface) you need awire.Bindsomewhere, or a provider that returns the interface. We use both styles — check existing patterns. - One provider per type. Wire refuses to compile if two providers produce the same type. Use
wire.Valueor split into distinct types if you need duplicates (e.g. two different*redis.Clientinstances). - Helper providers in
providers.go. Use them when you need to derive one config struct from another (e.g.ProvideGooglePlacesConfigextracts the Places API key from*config.Config) or when an upstream library doesn't have a Wire-friendly constructor.
Reading wire_gen.go¶
The generated file reads top-to-bottom in dependency order. Looking at it is the fastest way to answer "what does X actually depend on at runtime?" — you'll see the exact construction call with all parameters resolved.
For more on how wire_gen.go is wired into the binary, see backend/cmd/server/main.go — the very first thing main() does after loading config is call InitializeApp(cfg, db).