App
Clients
Create focused external-system clients without hiding useful Go APIs behind shadow wrappers.
The client boundary
Packages under clients/ may own provider authentication, SDK setup, HTTP
behavior, wire types, parsing, pagination, protocol errors, tracing hooks, and
shutdown details. Services own the business rules and use cases that consume
those integrations. Controllers, jobs, mailers, and MCP adapters normally call
services rather than low-level clients.
Clients are ordinary Go structs. Use New or Open when construction validates
configuration, installs hooks, prepares a transport, or establishes a
resource. Allow direct struct construction or a useful zero value when no
constructor invariant exists. Read environment variables in init/ and pass
explicit configuration to the client.
Do not wrap every external dependency. Add an application-owned boundary only when it owns useful behavior or semantics. Prefer direct Go types over wrappers that merely rename, embed, or forward an existing API.
Choose the exposed type
Prefer service-owned request and result types when the value has business meaning, enforces domain invariants, combines multiple sources, translates provider errors into domain behavior, or needs to remain stable while a provider changes.
Reuse a type from clients/<provider> when it is already a normalized
integration value and another service type would copy the same fields without
adding meaning. This is especially useful when multiple services legitimately
consume the same external-system value.
Expose an upstream type or upstream client directly when it is already the canonical Go abstraction, the application needs a large part of its API, or a wrapper would mostly contain one-line forwarding methods. Streaming, transactions, batching, buffers, or another measured performance concern can also justify preserving the original type.
This makes *pgxpool.Pool, *http.Client, io.Reader, and fs.FS reasonable
application dependencies. A clients/postgres package may configure a pool,
install tracing hooks, check startup, and return pool.Close without wrapping
Query, Exec, Begin, and every other pool method.
Memory or copying cost should be measured rather than assumed. Pointers, slices, readers, and iterators often preserve efficiency without exposing an entire provider SDK.
Interfaces only when useful
A service may accept a concrete client. Define the smallest interface in the consuming package only when multiple implementations, provider replacement, or focused service tests make substitution valuable. Implement only the methods the application uses; do not reproduce a complete vendor SDK in an application interface.
Initialization and shutdown
Construct clients in init/dependencies.go. Register a client with
lazydeps.Service when it owns a pool, goroutine, transport, file, listener,
or another resource that requires cleanup. Use the client's Ref.Use() inside
the dependent service initializer so GoLazy records the dependency and stops
it after the service.
Do not add a context helper for every client. Application context should normally expose services. Put a client in context only when runtime code genuinely resolves it directly, such as an explicitly presentation-specific OAuth flow.
Avoid shadow abstractions
Reconsider the boundary when it creates a struct that duplicates every SDK field, methods that only forward to identically named upstream methods, an interface matching a concrete client's full method set, or an embedded client used only to recover methods omitted by the wrapper.
Continue with Dependencies And Services for dependency wiring and PostgreSQL for the standard pool integration.