pgx v5-native PostgreSQL client with launcher lifecycle, health check, unit-of-work via context injection, and structured error mapping. What's included: - Executor / Tx / Client / Component interfaces using pgx native types (pgconn.CommandTag, pgx.Rows, pgx.Row) - New(logger, cfg) constructor; pgxpool initialised in OnInit - Config struct with env-tag support for all pool tuning parameters - UnitOfWork via context injection; GetExecutor(ctx) returns active Tx or pool - HandleError mapping pgerrcode constants to xerrors codes (AlreadyExists, InvalidInput, NotFound, Internal) - health.Checkable at LevelCritical; HealthCheck delegates to pgxpool.Ping Tested-via: todo-api POC integration Reviewed-against: docs/adr/
2.5 KiB
ADR-002: Local Executor Interface
Status: Accepted Date: 2026-03-18
Context
The Executor interface — the common query interface shared by the connection pool and an active transaction — must be defined somewhere. Earlier iterations of this codebase placed it in a shared dbutil package that both postgres and mysql imported. This created a cross-cutting dependency: every database module depended on dbutil, and dbutil had to make choices (e.g., which type system to use) that were appropriate for only one of them.
dbutil was eliminated as part of the monorepo refactor (see plan/decisions.md).
Decision
The Executor interface is defined locally inside the postgres package:
type Executor interface {
Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)
Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)
QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
}
The mysql package defines its own separate Executor using database/sql types. The two are not interchangeable by design — they represent different type systems.
Tx extends Executor with Commit(ctx context.Context) error and Rollback(ctx context.Context) error. Client provides GetExecutor, Begin, Ping, and HandleError. Component composes Client, launcher.Component, and health.Checkable.
Repository code in application layers should depend on postgres.Executor (or the higher-level postgres.Client) — not on the concrete *pgxpool.Pool or pgTx types.
Consequences
- Positive: No shared
dbutildependency. Each database module owns its interface and can evolve it independently. - Positive: The interface methods use pgx-native types, so there is no impedance mismatch between the interface and the implementation.
- Positive: Mocking
postgres.Executorin tests requires only implementing three methods with pgx return types — no wrapper types needed. - Negative: If a project uses both
postgresandmysql, neither module'sExecutoris compatible with the other. Cross-database abstractions must be built at the application domain interface layer, not by sharing a commonExecutor. - Note:
pgComponentitself also implementsExecutordirectly (forwarding to the pool), which means a*pgComponentcan be used wherever anExecutoris expected without callingGetExecutor. This is intentional for ergonomics in simple cases where no transaction management is needed.