Skip to content

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:

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.go tells 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:

  1. Write the constructor. E.g. services.NewSearchService(db *gorm.DB, redis services.RedisService) *services.SearchService. Constructor parameters define what Wire injects.
  2. Add it to the appropriate set. Open providers.go and append to ServiceSet (or RepositorySet, etc.):
var ServiceSet = wire.NewSet(
    // ...
    services.NewSearchService,
)
  1. 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)),
  1. Wire it into an aggregate. If the new dependency is used by App directly (rare), add it to App/NewApp. If it's used by a handler, add it to that handler's constructor and to the Handlers aggregate.

  2. Regenerate: task gen:wire.

  3. 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 a wire.Bind somewhere, 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.Value or split into distinct types if you need duplicates (e.g. two different *redis.Client instances).
  • Helper providers in providers.go. Use them when you need to derive one config struct from another (e.g. ProvideGooglePlacesConfig extracts 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).