ADR-0001: Zero-dependency core; dependencies isolated in adapters¶
Status¶
Accepted.
Context¶
A resilience library is infrastructure: it sits on the hot path of every request
in the services that adopt it. Every transitive dependency it pulls in becomes a
dependency of those services — a larger attack surface, more CVE exposure, more
version-conflict friction, and a heavier go.mod graph. Many teams reject
infrastructure libraries purely on their dependency footprint.
The library nonetheless needs to integrate with external systems for its optional features: Redis for distributed rate limiting, gRPC/HTTP frameworks for middleware, and Prometheus/OpenTelemetry for observability. Those integrations require real third-party packages.
Decision¶
The core algorithm and resilience packages have zero external runtime dependencies. External dependencies are permitted only in clearly isolated adapter layers:
- Distributed backend lives behind
ratelimit/store. The Redis adapter (ratelimit/store/redis.go) is the only place that importsgithub.com/redis/go-redis/v9; the core algorithms talk to thestore.Storeinterface (ratelimit/store/store.go), never to Redis directly. - Framework middleware (chi, gin, echo, fiber, connect) lives in a
separate
contribmodule (contrib/go.mod) so those framework dependencies never enter the core module's graph. See docs/STABILITY.md. - Observability adapters (
metric/prometheus,observability/otel,observability/otelhttp) isolate the Prometheus / OpenTelemetry dependencies; the core emits through a small no-op-defaultmetric.Recorderinterface (metric/metric.go) rather than importing a metrics SDK.
The rule is enforced in CI, not just by convention. The verify-deps Make
target (Makefile, target verify-deps) runs go list over an explicit list of
core packages — ratelimit and its algorithm subpackages, circuitbreaker,
bulkhead, retry, retry/backoff, timeout, fallback, pipeline,
loadshed, concurrency, metric, and internal/clock / internal/atomicx —
and fails the build if any of them imports a non-stdlib, non-internal package.
The CI verify-zero-deps job (.github/workflows/ci.yml) invokes it on every
push.
Consequences¶
Positive:
- Adopters who use only the local (in-memory) algorithms pull in no external runtime dependencies — the marketing claim on pkg.go.dev is provable, not aspirational.
- The zero-dep boundary is a tested invariant: a PR that accidentally imports Redis into a core algorithm package fails CI, so the guarantee can't silently erode.
- Redis, gRPC, and metrics SDKs are opt-in — you pay for them only if you import the adapter that needs them.
Negative / trade-offs:
- The abstraction has a cost: distributed atomicity must be expressed through the
narrow
store.Storeinterface and Lua scripts (see ADR-0003) rather than by reaching for a rich Redis client API directly. - Observability is indirect: the core fires through the
metric.Recorderinterface instead of calling a Prometheus client inline, which adds a small layer of indirection. - Multi-module layout (
contribas its own module) adds release and versioning complexity —contribis tagged and versioned independently of the core.