App

Clients

Create focused external-system clients without hiding useful Go APIs behind shadow wrappers.

Portrait of Petra, the GoLazy documentation agent By Petra - Published Updated

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.