Skip to content

Table of Contents

ADR 0001 — Layered architecture with the repository pattern

  • Status: Accepted
  • Date: 2026-06-26

Context

The Bookstore service must expose CRUD HTTP endpoints backed by PostgreSQL while staying testable, swappable, and easy to reason about. We needed to decide how to structure the code: a flat "fat handler" approach that talks to the database directly, an ORM-centric design, or an explicitly layered architecture.

Decision

We use a strict three-layer design — handler → service → repository — organized as vertical feature slices (internal/book, internal/author). Dependencies point inward and are expressed as interfaces defined by the consumer:

  • The handler depends on a service interface it declares.
  • The service depends on a repository interface it declares.
  • The repository depends only on a tiny DBTX interface satisfied by *pgxpool.Pool.

cmd/api/main.go is the composition root: it constructs the concrete implementations and wires them together. We keep three models — domain struct, persistence row, and API DTO — with mapping functions at each boundary. Driver errors are translated to domain sentinel errors in the repository and to HTTP status codes only at the transport boundary (internal/httperr).

Alternatives considered

  • Fat handlers (HTTP + SQL in one place): fastest to write, but business rules and SQL leak into the transport layer, making unit testing require a real DB and coupling the wire format to the schema.
  • ORM (e.g. GORM): convenient, but hides SQL, complicates performance tuning, and couples the domain to the ORM's model tags. We chose pgx + sqlc-style raw SQL for transparency and compile-time-checked queries.

Consequences

  • Positive: each layer is testable in isolation (mocked repo for service tests, mocked service for handler tests, real Postgres via testcontainers for repository tests). Persistence and transport details are swappable. Errors have one canonical mapping to HTTP.
  • Negative: more boilerplate — interfaces and mapping functions per feature. For a domain this simple the indirection is arguably overkill, but it is exactly the pattern that pays off as services grow, which is the point of this exercise.