Skip to content

Dependency State

wiki edited this page Sep 4, 2026 · 1 revision

Dependency state

A dependency is anything the application needs and does not control: a database, a cache, a queue, another service. The state store holds what is currently known about each one.

type DepStateStore interface {
	Get(id string) *DepState
	GetAll() map[string]*DepState
	Register(id string) *DepState
	ReportSuccess(id string, latency time.Duration)
	ReportFailure(id string, errType string)
	SetStatus(id string, status Status, message string)
	SetCircuitState(id string, circuitState CircuitState)
	Remove(id string)
}
store := ext.StateStore()

The four statuses

Meaning IsHealthy()
StatusUp fully operational true
StatusDegraded operational, with problems true
StatusDown not operational false
StatusUnknown never determined false

Status marshals to and from a string in JSON — "UP", "DEGRADED" — so responses are readable and stable if the constants are ever reordered.

Three ways state gets written

1. Health checks

An active check writes its result into the store on every tick. This is the usual path.

2. Reporting from real traffic

store.ReportSuccess("payments-api", latency)
store.ReportFailure("payments-api", "timeout")

Better signal than a synthetic ping, because it reflects the operations you actually perform. A DepReporter bundles the two with an optional circuit breaker:

rep := health.NewDepReporter("payments-api", store, breaker)
rep.ReportSuccess(latency)
rep.ReportFailure("timeout")

3. A wrapped HTTP client

client := health.WrapHTTPClient(http.DefaultClient, "payments-api", store,
	health.WithHTTPTimeout(3*time.Second),
	health.WithRetries(2, 200*time.Millisecond),
	health.WithCircuitBreaker(breaker),
)

resp, err := client.Get(ctx, "https://payments.internal/v1/charges")

Every call reports its outcome and latency automatically, and the circuit breaker refuses calls while it is open — returning *CircuitOpenError rather than making a request that is going to fail.

Thresholds

health.WithStateStoreConfig(health.DepStateStoreConfig{
	FailureThreshold:  5,                // consecutive failures → DOWN
	DegradedThreshold: 2,                // consecutive failures → DEGRADED
	WindowDuration:    30 * time.Second, // the counting window
})

Those are the defaults. Two consecutive failures mark a dependency degraded; five mark it down; the counters are scoped to a 30-second window, so isolated failures spread over minutes do not accumulate into an outage.

Tune FailureThreshold against your traffic rate. On an endpoint doing 1000 requests a second, five consecutive failures is 5 milliseconds of trouble — you probably want a higher threshold or a circuit breaker with a longer window.

What a state carries

type DepState struct {
	ID           string
	Status       Status
	Message      string
	LastCheck    time.Time
	LastSuccess  *time.Time
	LastFailure  *time.Time
	FailureCount int64
	SuccessCount int64
	LastLatency  time.Duration
	AvgLatency   time.Duration
	Metadata     map[string]string
	CircuitState CircuitState
}

All of it appears under dependencies on /status — which is why that endpoint should not be public.

Clone() returns a thread-safe copy; the store hands out clones rather than live pointers, so a reader cannot race a writer.

SetMeta / GetMeta attach arbitrary strings — a region, a replica identifier, a schema version.

State in the request context

For handlers that want to adapt rather than be refused — serve stale data, skip an enrichment step, degrade a response — inject state into the context without gating:

app.UseOnRouter(rex.DefaultRouterName,
	health.DepContextMiddleware(store, "cache", "recommendations"),
	rextension.PriorityDefault,
)

func handler(ctx rxroute.Context) {
	if health.IsDegraded(ctx, "recommendations") {
		_ = ctx.JSON(200, responseWithoutRecommendations())
		return
	}
	…
}
Helper
GetDepState(ctx, depID) the full state
IsDegraded(ctx, depID) is it marked degraded
GetDepStateContext(ctx) route id, all states, degraded ids

This is a soft mechanism — nothing is refused. To refuse, use the gate.

Naming

Dependency identifiers are free strings, matched exactly between Dependencies() on a route, ReportFailure calls, and check names when you use NewDependencyCheck. A typo produces a dependency that is permanently StatusUnknown — which, with the default TreatUnknownAs: StatusUp, means the gate silently serves.

Declare them as constants:

const (
	DepDatabase = "database"
	DepPayments = "payments-api"
)

Clone this wiki locally