Packages, modules and tooling · 3 MIN READ
Package initialization is not a request-time connection factory
Move fallible setup to an explicit owner and close shared resources after work drains.
Imported packages initialize before main, and hidden setup can turn an import into a database connection, network wait or process-wide configuration decision. Make those dependencies visible at the composition root so startup, tests, retries and shutdown share a clear owner.
Packages and production tooling · A MAP OF THE IDEA
- Load configuration
- Construct shared client
- Serve bounded work
- Drain and close
Useful when
- Move fallible setup to an explicit owner and close shared resources after work drains.
Watch out
- The application component owns a shared client across requests, stops new work and drains existing work before closing it. Per-request pools multiply connection and startup costs.
Implementation & operation cost: Explicit startup and drain ordering take code, but make connection ownership, failure and tests observable.
Keep fallible startup explicit
A constructor can return an error and accept a caller-owned context; init cannot return an error to its caller. Build shared clients once, check readiness with a bounded operation, and inject them into handlers. Do not construct a new pool every time a request arrives.
func run(ctx context.Context) error {
cfg, err := loadConfig()
if err != nil { return fmt.Errorf("config: %w", err) }
store, err := openStore(ctx, cfg)
if err != nil { return fmt.Errorf("store: %w", err) }
defer store.Close()
return serve(ctx, store)
}This is an application-structure fragment: loadConfig, openStore and serve belong to the application. The ordering exposes error propagation and ownership, while real shutdown must let active handlers finish before the shared store closes.
Audit hidden work in dependencies
Look for init functions, package-level constructors, mutable registries and side-effect imports. Some registration is intentional; expensive or fallible runtime work needs a visible reason. An import cycle is a package-graph problem, not a request concurrency problem.
go list -deps ./cmd/app
go list -f '{{.ImportPath}}: {{.Imports}}' ./internal/...
go test -count=1 ./...The commands show dependency paths and exercise a fresh test run. They do not inspect hidden effects automatically; combine the graph with reading constructors and initialization code at the boundaries you own.
Budget readiness and shutdown
A readiness check should answer whether this instance can serve useful work, not hammer every dependency on every health probe. Stop new work, drain in-flight requests within a deadline, then close shared clients. Use a new bounded cleanup context when the request context is already canceled.
TAKE THIS WITH YOU
Give fallible setup, request work and cleanup explicit owners. Reuse shared clients, preserve deadlines and test startup failure and shutdown ordering.
Check your understanding
Who should own a service-wide database client?
Go to the source
Progress stays in this browser. No account needed.