Day 162 — Capstone: Architecture & Setup¶
Month 6 · Week 4 · ⬅ Day 161 · Day 163 ➡ · Journal index
🎯 Learning Objective¶
Scaffold the capstone — linkr, a production URL shortener — with a clean ports-and-adapters layout so business logic depends on interfaces it owns, never on Postgres, Redis, or HTTP directly.
📚 Topics¶
- Hexagonal / ports-and-adapters layering · dependency inversion
- Standard Go project layout (
cmd/,internal/) · composition root inmain
📖 Reading / Sources¶
- Effective Go — Interfaces
- Go blog — Organizing a Go module
- Standard Go Project Layout (community) (read critically — it's a convention, not law)
📝 Notes¶
- linkr does one thing:
POSTa long URL → get a shortcode;GET /{code}→ redirect + count a hit. Small enough to finish, real enough to exercise the whole month. - Layers: transport (REST + gRPC) → service (business rules) → repository port (an interface the service owns) → adapters (Postgres, Redis, in-memory). Dependencies point inward → [[dependency-inversion]].
- "Accept interfaces, return structs." The
Serviceaccepts aRepositoryinterface;NewServicereturns a concrete*Service. Callers inmainchoose the adapter → [[interfaces]]. - The composition root is
cmd/linkr/main.go: the only place that knows about concrete infra. SwappingmemRepoforpgRepois a one-line change there, nothing else. internal/makes packages un-importable outside the module — use it for everything that isn't a deliberate public API → [[internal-packages]].- Validation is a business rule → it lives in the service, not the HTTP handler and not the DB. The transport only translates wire formats.
- Sentinel errors (
ErrNotFound) are defined at the port; adapters wrap them with%wso callers canerrors.Isregardless of backend → [[error-wrapping]].
💻 Code Examples¶
// Repository is a PORT: the interface the Service owns. Adapters satisfy it.
// Context is the first parameter on every method so requests are cancellable.
type Repository interface {
Save(ctx context.Context, l Link) error
ByCode(ctx context.Context, code string) (Link, error)
IncHits(ctx context.Context, code string) error
}
type Service struct{ repo Repository } // depends on the interface, not pgx
func NewService(repo Repository) *Service { return &Service{repo: repo} } // return struct
Full runnable layering demo:
examples/month-06/architecture· Run:go run ./examples/month-06/architecture
Planned tree (the capstone repo lives under projects/linkr/):
cmd/linkr/main.go # composition root: wire adapters, start server
internal/link/ # domain: Link, Service, Repository (the port)
internal/store/postgres # adapter: pgx-backed Repository
internal/store/memory # adapter: in-memory Repository (tests/dev)
internal/cache/redis # adapter: cache-aside in front of the repo
internal/transport/rest # chi handlers
internal/transport/grpc # gRPC server
internal/obs/ # slog, metrics, tracing setup
🏋️ Exercises / Practice¶
| Exercise | Status | Link |
|---|---|---|
shortcode — base62 id ↔ code round-trip |
✅ | exercises/month-06/week-4/shortcode |
linkstore — context-first in-memory repository |
✅ | exercises/month-06/week-4/linkstore |
🐛 Mistakes Made¶
- First draft put the
Repositoryinterface in thepostgrespackage — that inverts the dependency the wrong way. The consumer (service) owns the interface; the adapter imports the domain, not the reverse. - Returned
Repository(interface) fromNewServiceout of habit → makes mocking the service harder and hides the concrete type. Return the struct.
❓ Open Questions¶
- Where should DTO↔domain mapping live — in transport or a dedicated mapper? (Leaning: transport, since DTOs are a wire concern.)
🧠 Active Recall (answer without looking)¶
- Q: Which package should declare the
Repositoryinterface — the service or the Postgres adapter, and why?A
The service (consumer) declares it. Go interfaces are satisfied implicitly, so the consumer states the minimum it needs and any adapter can satisfy it without importing the consumer. This keeps dependencies pointing inward toward the domain.
2. Q: What is the "composition root" and why keep infra wiring there? A
It's the single place (here cmd/linkr/main.go) that constructs concrete dependencies and injects them. Centralizing it means the rest of the code depends only on interfaces, so swapping an adapter (memory → Postgres) is a one-line change and unit tests can inject fakes.
🪶 Feynman Reflection¶
A clean architecture is just disciplined arrow-direction: the important code (business rules) never points at the replaceable code (databases, web frameworks). It points at small interfaces it defines, and the replaceable code points back by implementing them. That inversion is what lets me test the core with no database running and swap infrastructure later without a rewrite.
🕳️ Knowledge Gaps¶
- Generated gRPC code placement vs
internal/boundaries — resolve tomorrow when I wire the proto.
✅ Summary¶
linkr is scaffolded as a ports-and-adapters service: domain + service + a repository port, with the composition root in main. The shape is proven by the stdlib architecture example and the linkstore exercise.
⏭️ Next Steps / Prep for Tomorrow¶
- Day 163: build the REST handlers (chi) and the gRPC API over the same
Service.
| Time spent | Difficulty | Confidence |
|---|---|---|
| 90 min | 🟦🟦⬜⬜⬜ | 🟦🟦🟦⬜⬜ |
Suggested commit: feat(linkr): scaffold ports-and-adapters architecture (day 162)