Skip to content

Weekly Review — Month 5 · Week 3 (Days 127–133)

Journal index · Roadmap › this week

📅 The Week in One Line

Moved from "a gRPC service that runs" to "a service built to last": hexagonal core, dependency injection (by hand and with wire), domain value objects/entities, ADRs for the why, and cache-aside with Redis.

✅ What I Completed

  • Day 127 — Hexagonal architecture / ports & adapters (dependencies point inward)
  • Day 128 — Manual dependency injection (constructor injection, composition root)
  • Day 129 — DI with wire (compile-time codegen: providers, sets, injectors, wire.Bind)
  • Day 130 — Domain modeling & boundaries (value objects vs entities, invariants)
  • Day 131 — Architecture Decision Records (Nygard template, immutable log)
  • Day 132 — Redis cache-aside (read-through, TTL, delete-on-write, stampede)
  • Day 133 — Review + active recall
  • Mini-project: stdlib models of a hexagonal core, hand-wired DI, value objects, and a TTL cache-aside cache
  • Exercises solved: 3 (ports, money, cacheaside) — all green

💡 Lessons Learned

  • Define the interface next to its consumer. In Go the core owns the port and the adapter implements it; that single habit keeps the dependency arrow pointing inward and avoids import cycles.
  • DI is just passing arguments. A constructor's parameter list is its dependency contract; inject interfaces (clock, repo, cache) to create test seams and prefer fakes over generated mocks.
  • wire is codegen, not a container. It emits the same main-wiring I'd type by hand, fails at compile time, and adds zero runtime cost — and wire.Bind connects a concrete to its port.
  • Make invalid states unrepresentable. Unexport fields, validate in the constructor, return new values from methods; comparable structs then give value equality for free, and money is integer cents, never float.
  • ADRs preserve the why. Context + Consequences are what code can't tell you later; keep them short and immutable — supersede, never edit.
  • Cache-aside keeps Redis out of the core. Read-through on miss, populate with a TTL, delete (don't rewrite) on write, branch on redis.Nil, and never cache failures for long.

💪 Strengths (what clicked)

  • The "core owns the port, adapter plugs in at the composition root" mental model — it generalized cleanly across the repo, the clock, and the cache.
  • Value objects: validating constructor + unexported fields + immutability felt natural and made tests trivial.

🧩 Weaknesses (what's still fuzzy)

  • Unit-of-work / transactions spanning two repository calls without leaking SQL into the core.
  • Cache-stampede defenses (singleflight vs probabilistic early expiry vs a Redis lock key).
  • wire ergonomics for two same-typed dependencies and cross-package provider sets.

🔁 Spaced-Repetition Re-quiz (topics from earlier weeks)

  1. Q: (M5 W2 — interceptors) In what order do you chain logging, recovery, and auth interceptors, and why?
    ALogging (outermost) → recovery → auth (innermost). Logging sees everything including recovered errors; recovery catches panics from auth and the handler; auth runs last before the handler so it can reject early.
  2. Q: (M5 W1 — gRPC errors) Why return status.Error(codes.X, msg) instead of a plain error?
    ASo callers can branch on the canonical code (status.Code(err)) and a gateway can map it to the right HTTP status — string-matching the message is brittle.
  3. Q: (M4 — concurrency) What happens on a send/receive on a nil channel?
    AIt blocks forever. Useful to disable a select case by setting its channel to nil.
  4. Q: (M3 — errors) How does errors.Is differ from ==?
    AIt walks the wrap chain via Unwrap, matching a wrapped sentinel anywhere in the chain — e.g. an ErrNotFound wrapped with %w.
  5. Q: (M1 — interfaces) Why can a non-nil interface hold a nil pointer and not equal nil?
    AAn interface is (type, value); a nil *T gives it a non-nil type word, so iface != nil even though the pointer is nil — the nil-interface trap.

🎯 Action Items

  • Sketch a unit-of-work helper so a service can run two repository writes in one transaction without importing database/sql into the core.
  • Add singleflight to the cache-aside Get and benchmark it under a simulated stampede.
  • Write two real ADRs in docs/adr/ (gRPC-for-internal-APIs, hexagonal-layout) and number them.
  • Convert the hand-wired composition root into a wire provider set and diff the generated file.

🚀 Next Week Goals

  • Observability: structured logging (slog), request-scoped fields, and log levels.
  • Metrics (RED/USE), Prometheus exposition, and gRPC/HTTP instrumentation.
  • Distributed tracing (OpenTelemetry) and context propagation across services.
  • Health checks, readiness/liveness, and graceful shutdown.

📊 Metrics

Hours Days hit Exercises Commits Avg confidence
10.5 7/7 3 7 3.⅗

Suggested commit: docs(journal): month 5 week 3 review