Skip to main content

What Happened

The choco-consumers service had 7 handler interfaces (AnalyticsWriter, SearchIndexer, LedgerWriter, etc.) all backed by log-only stubs. Replaced all 7 with real PostgreSQL-backed adapters in a single TASKSET without modifying any handler code.

Key Insight

Define interfaces in the handler package, implement in the adapter package. The handler owns the contract (Write(ctx, eventType, data)) — it doesn’t know or care whether the adapter writes to PostgreSQL, ClickHouse, or /dev/null. This made the swap a main.go-only change: replace &stubAnalyticsWriter{} with adapters.NewPgAnalyticsWriter(pool).

The Fallback Rule

When DATABASE_URL is empty, the service falls back to stub implementations automatically. This means:
  • Local dev works without PostgreSQL
  • CI tests run without infrastructure
  • Production uses real adapters with no code path difference
This pattern eliminates the “works in dev, fails in prod” class of bugs because the handler logic is identical in both paths — only the adapter differs.

Reusable For

Any event-driven consumer service that needs to support multiple backend targets (PostgreSQL → ClickHouse, PostgreSQL → Tantivy gRPC, etc.) without touching handler logic. The pattern scales to any number of backends.