Table of Contents
- ADR 0001 — Layered architecture with the repository pattern
- Context
- Decision
- Alternatives considered
- Consequences
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
serviceinterface it declares. - The service depends on a
repositoryinterface it declares. - The repository depends only on a tiny
DBTXinterface 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.