Skip to content

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.

AppSet
↓ aggregates
Platform singletons
InfrastructureSet
DB, Redis, storage, assets
ServiceSet
cache, audit, email, taxonomy, llm, ws hubs
AsyncSet
asynq + handlers + gameengine
Per-domain WireSets (one per domain)
Store
NewEventStore
Service
NewEventService
Handler
NewEventHandler
Cross-domain edges (event needs chat, presence needs event, ...) are declared as narrow interfaces in the consuming domain's 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.go shows 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.SessionHub and *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

  1. Write the constructor in the domain package, named New<Thing>. Its parameters are what Wire injects.
  2. Add it to the domain's WireSet in services/<domain>/wire.go. For a brand-new cross-cutting singleton, add it to ServiceSet in providers.go instead.
  3. Bind to an interface if a sibling domain consumes it. Declare the interface in the consumer's deps.go, then add wire.Bind(new(consumer.Iface), new(*provider.Concrete)) to AppSet.
  4. Surface a handler. A new Handler goes into NewHandlers' parameter list and the Handlers struct, and its routes into router.go.
  5. Regenerate + build: task gen:wire then task 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 ShareBaseURL wrappers) rather than sharing a bare string.
  • Helper providers. Use them to derive one config struct from another (ProvideGooglePlacesConfig) or to adapt a library that has no Wire-friendly constructor (asyncDeleteEnqueuer adapting *async.Client to assets.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.