Files
todo-api/docs/adr/ADR-001-n-layer-architecture.md
Rene Nochebuena 3fcba82448 feat(todo-api): add full-stack POC demonstrating micro-lib v0.9.0
Runnable REST API exercising every micro-lib tier in a containerless setup: N-layer architecture, SQLite persistence, header-based auth simulating Firebase output, and bit-mask RBAC enforcement.

What's included:
- cmd/todo-api: minimal main delegating to application.Run
- internal/application: full object graph wiring — launcher, sqlite, httpserver, httpmw stack, routes in BeforeStart
- internal/domain: User entity, ResourceTodos constant, PermReadTodo/PermWriteTodo bit positions
- internal/repository: TodoRepository, UserRepository, DBPermissionProvider (SQLite via modernc)
- internal/service: TodoService, UserService with interface-based dependencies
- internal/handler: TodoHandler, UserHandler using httputil adapters and valid for input validation
- internal/middleware: Auth (X-User-ID → rbac.Identity) and Require (bit-mask permission gate)
- logAdapter: bridges logz.Logger.With return type to httpmw.Logger interface
- SQLite schema: users, user_role (bitmask), todos; migrations run in BeforeStart
- Routes: POST /users (open), GET+POST /todos (RBAC), GET /users (RBAC)

Tested-via: todo-api POC integration
Reviewed-against: docs/adr/
2026-03-19 13:55:08 +00:00

2.9 KiB

ADR-001: N-Layer Architecture

Status: Accepted Date: 2026-03-18

Context

The todo-api is a POC application demonstrating the full micro-lib stack. It needs an internal structure that is illustrative, maintainable, and cleanly testable. The question is how to organize the application code within the internal/ directory.

The project operates on the following rules:

  • Dependencies must flow inward: outer layers may depend on inner layers, inner layers must not depend on outer layers.
  • Domain types are the shared vocabulary — they may be imported by all layers.
  • All interfaces are defined at the layer boundary where they are consumed (not where they are implemented).

Decision

The application uses a five-layer architecture within internal/:

cmd/todo-api/main.go
    └── internal/application/    (wiring: constructs and connects all components)
            ├── internal/handler/     (HTTP: decodes request, calls service, encodes response)
            │       ├── internal/service/     (business logic: validates, orchestrates)
            │       │       ├── internal/repository/  (persistence: SQL queries)
            │       │       │       └── internal/domain/  (entities, constants, no dependencies)
            │       │       └── internal/middleware/  (HTTP middleware: auth, RBAC)
            │       └── (domain types shared across layers)

Dependency rules enforced:

  • domain imports nothing from internal/.
  • repository imports domain and infrastructure (sqlite, rbac). It defines its own interfaces (TodoRepository, UserRepository).
  • service imports domain and repository interfaces. It does not import concrete repository types.
  • handler imports domain, service interfaces, and httputil/valid. It does not import repository or service concrete types.
  • middleware imports rbac and repository interfaces. It does not import service or handler.
  • application imports everything and wires it together. It is the only package with knowledge of the full object graph.

The cmd/todo-api/main.go entry point contains only application.Run() — it is the thinnest possible main.

Consequences

  • Each layer is independently testable with mocks of the layer below.
  • The application package is the only place where concrete types cross layer boundaries. Changing a repository implementation requires changes only in application and repository.
  • Service interfaces (TodoService, UserService) are defined in the service package (where they are implemented), not in handler (where they are consumed). This follows Go convention: accept interfaces, not concrete types — but define them close to the implementation.
  • The middleware package sits alongside handler rather than below it in the dependency chain. Both are HTTP-layer concerns; neither depends on the other. application wires them together on the router.