Wiring (Dependency Injection)¶
The backend assembles its object graph with Google Wire, compile-time dependency injection. Every New<Thing> constructor is grouped into a provider set; Wire generates wire_gen.go from those sets, producing a single InitializeApp that builds the whole graph in topological order.
Source files (backend/internal/wiring/):
| File | Role |
|---|---|
providers.go |
Cross-cutting provider sets, cross-domain wire.Bind edges, and the App / Handlers aggregates |
router.go |
SetupRouter composes every domain's RegisterRoutes into the /api/v1 tree and mounts the WebSocket group |
wire.go |
The wire.Build(AppSet) directive (build tag wireinject). Lives in the wiring package so the endpoint-test harness boots the same graph; cmd/server/app.go is a thin delegation |
wire_gen.go |
Generated. Do not edit |
Mental model¶
Wiring has three tiers. Platform singletons are shared by everyone; each domain owns a self-contained vertical set; a handful of cross-domain interface edges are bound at the top.
DB, Redis, storage, assets
cache, audit, email, taxonomy, llm, ws hubs
asynq + handlers + gameengine
NewEventStore
NewEventService
NewEventHandler
deps.go and bound with wire.Bind at AppSet, where both concrete providers are reachable.Why Wire¶
- Compile-time checks. A missing dependency, a cycle, or a duplicate binding fails
go build, not startup. - Zero runtime cost. The output is plain
New*calls in dependency order, no reflection. - Explicit object graph.
wire_gen.goshows exactly how the app is assembled.
Provider sets¶
providers.go defines the cross-cutting sets; each domain package under backend/internal/services/<domain>/ exports its own WireSet in wire.go. AppSet aggregates all of them.
| Set | Lives in | What it provides |
|---|---|---|
ConfigSet |
providers.go |
Per-domain config extractors (ProvideJWTConfig, ProvideStripeConfig, ProvideGooglePlacesConfig, ...) and scalars (ProvideFrontendURL, ProvideEncryptionKey, ProvideWorkerConcurrency) |
InfrastructureSet |
providers.go |
*pgxpool.Pool (via ProvidePool), the shared *redis.Client (also bound as redis.UniversalClient), storage.FileStorage, and the assets.Service (bound as user.AssetDeleter) |
RepositorySet |
providers.go |
The cross-cutting repos not owned by a single domain (repository.NewAuditRepository, repository.NewCoExperienceRepository) |
ServiceSet |
providers.go |
Platform singletons: cache.NewCache, audit.NewService, email.NewService, taxonomy.NewService, the semantic resolver + search enricher (platform/llm), notify push dispatcher, webhooks.NewService, the OTP service, the per-domain share-URL wrappers, the privacy loader, and both WebSocket hubs (ws.NewSessionHub, ws.NewClientHub) |
<domain>.WireSet |
services/<domain>/wire.go |
That domain's New<Thing> constructors: Store, Service, Handler (multi-aggregate domains prefix them, e.g. NewEventStore / NewEventLifecycleService) |
HandlerSet |
providers.go |
NewHandlers, the aggregate every HTTP handler is collected into |
AsyncSet |
providers.go |
The Asynq Client / Server / Scheduler / App, the async HandlerService, and the game engine (Signal, Engine, expiry + replay), plus the binds that let async handlers reach domain services |
Domains wired via their own WireSet: admin, auth, chat, content, discovery, event, friend, location, maps, media, moment, notification, partner, passport, payment, plan, presence, user. The game engine is wired inside AsyncSet because it is driven by async signals.
AppSet composes the sets above, adds ProvideRateLimiter + ProvideIPBlocker + NewApp, and declares every cross-domain wire.Bind.
Cross-domain binds¶
A domain never imports a sibling domain's concrete types. When domain A needs behavior from domain B, A declares a narrow interface in its deps.go and B's concrete type is bound to it at AppSet. Examples from providers.go:
| Interface (consumer) | Concrete (provider) |
|---|---|
event.ChatSystemSender |
*chat.Service |
event.MessagePurger |
chat.MessagePurger |
user.EventCleaner |
event.EventLifecycleService |
presence.EventCheckinMarker |
event.EventService |
maps.EventDetailProvider |
event.EventService |
moment.TravelRecorder |
passport.PassportService |
passport.TravelLogSubscriber |
gameengine.Signal |
The same pattern disambiguates duplicate primitive types: the public share origin is a plain string, so each domain that needs it takes a typed wrapper (moment.ShareBaseURL, presence.ShareBaseURL, plan.ShareBaseURL) provided by its own Provide*ShareBaseURL helper.
Routing gates are parameters, not imports¶
A domain's routes.go never imports middleware or access. RegisterRoutes(r, handler, ...gates) takes a chi.Router plus its auth and rate-limit gates as func(http.Handler) http.Handler arguments, and router.go supplies them (middleware.JWTAuth, middleware.OptionalJWTAuth, app.RateLimiter.*, access.Require(...)). This keeps the domain packages free of the middleware graph.
App assembly¶
NewApp collects the runnable top-level components:
*Handlers(every HTTP handler)*async.App(Asynq server + scheduler + client)*ws.SessionHuband*ws.ClientHub*auth.AuthService,user.UserStore,partner.Store(for middleware)*middleware.RateLimiter,*middleware.IPBlocker
main.go calls InitializeApp(cfg, db), then wiring.SetupRouter(app, cfg, opts) and starts the hubs / async app per process mode.
Regenerating wire_gen.go¶
After any provider change (new constructor, new dependency on an existing constructor, new wire.Bind):
task gen:wire
# or, from backend/:
wire ./internal/wiring
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 are overwritten the next time wire runs.
Adding a dependency¶
- Write the constructor in the domain package, named
New<Thing>. Its parameters are what Wire injects. - Add it to the domain's
WireSetinservices/<domain>/wire.go. For a brand-new cross-cutting singleton, add it toServiceSetinproviders.goinstead. - Bind to an interface if a sibling domain consumes it. Declare the interface in the consumer's
deps.go, then addwire.Bind(new(consumer.Iface), new(*provider.Concrete))toAppSet. - Surface a handler. A new
Handlergoes intoNewHandlers' parameter list and theHandlersstruct, and its routes intorouter.go. - Regenerate + build:
task gen:wirethentask build:backend. A cycle, missing provider, or duplicate binding fails with a clear error.
Common gotchas¶
- Interface vs concrete. Wire injects the exact declared type. A consumer taking an interface needs a
wire.Bind(or a provider returning the interface). Both styles are in use, check the nearest example. - One provider per type. Two providers producing the same type is a compile error. Give duplicates distinct named types (the
ShareBaseURLwrappers) rather than sharing a barestring. - Helper providers. Use them to derive one config struct from another (
ProvideGooglePlacesConfig) or to adapt a library that has no Wire-friendly constructor (asyncDeleteEnqueueradapting*async.Clienttoassets.Enqueuer).
Reading wire_gen.go¶
The generated file reads top-to-bottom in dependency order, the fastest way to answer "what does X actually depend on at runtime?". main() calls InitializeApp(cfg, db) right after loading config; see backend/cmd/server/main.go.