Counter, Gauge, and Histogram metric types behind a common MetricsRegistry, sampling strategies for high-volume histograms, and the direct mapping to Micrometer's own design.
Published September 23, 2026
interface Metric { String name(); }
class Counter implements Metric {
AtomicLong value = new AtomicLong(0);
void increment() { value.incrementAndGet(); } // only ever goes UP
}
class Gauge implements Metric {
Supplier<Double> valueSupplier; // reads a CURRENT value on demand, can go up or down
double value() { return valueSupplier.get(); }
}
class Histogram implements Metric {
List<Double> samples = new CopyOnWriteArrayList<>(); // or a more compact summary structure at scale
void record(double value) { samples.add(value); }
double percentile(double p) { /* sorted samples, interpolate at percentile p */ }
}
class MetricsRegistry {
Map<String, Metric> metrics = new ConcurrentHashMap<>();
void register(Metric metric) { metrics.put(metric.name(), metric); }
}
These three types map directly onto real metric semantics from Metrics & Monitoring: Counter (monotonically increasing — a request count, never decrements), Gauge (a current point-in-time value that can go up or down — active connection count, queue depth), Histogram (a DISTRIBUTION of observed values — request LATENCY specifically, where you care about the shape, not just an average, directly connecting to Metrics & Monitoring's percentile-vs-average discussion).
class ReservoirSamplingHistogram implements Metric {
double[] reservoir = new double[1000]; // fixed-size sample, NOT every single value
int count = 0;
void record(double value) {
count++;
if (count <= reservoir.length) {
reservoir[count - 1] = value;
} else {
int j = ThreadLocalRandom.current().nextInt(count);
if (j < reservoir.length) reservoir[j] = value; // reservoir sampling: replace with decreasing probability
}
}
}
Recording EVERY single value for a high-volume histogram (millions of requests/sec) is both a memory problem (storing every value) and a computation problem (percentile calculation over an ever-growing list). Reservoir sampling maintains a FIXED-SIZE representative sample — new values replace existing reservoir entries with DECREASING probability as more values arrive, which is what keeps the sample statistically representative of the full stream's distribution even though only a small, bounded subset is actually stored. This is the practical answer to "discuss sampling for histograms at high volume vs recording every single value" — a small, well-chosen sample gives a statistically sound percentile estimate at a fraction of the memory/compute cost of tracking everything.
Spring Boot's Micrometer (already introduced in Metrics & Monitoring) exposes exactly this same three-type model — Counter, Gauge, and Timer/DistributionSummary (Micrometer's histogram-equivalents) — registered against a MeterRegistry that plays the identical role as this exercise's MetricsRegistry. Building this from scratch is what turns @Timed and meterRegistry.counter(...) from magic annotations into a concrete, understood mechanism.
Q: Why can't a Gauge simply be implemented the same way as a Counter, with increment/decrement methods?
A: It COULD be, but the Supplier<Double>-based approach shown (reading a current value ON DEMAND rather than maintaining running state) is often preferable for values that are already tracked elsewhere (e.g. connectionPool.getActiveCount()) — it avoids DUPLICATING state that already exists in the thing being measured, reading it fresh each time the metric is scraped instead.
Q: Does reservoir sampling bias the percentile estimate in any way? A: A correctly-implemented reservoir sampling algorithm (as shown, with the replacement probability decreasing appropriately as count grows) gives an UNBIASED uniform random sample of the full stream — the key correctness property is that every value seen so far has an EQUAL probability of being in the final reservoir, which the specific replace-with-probability-length/count formula guarantees.
Q: How would you export these in-memory metrics to an external system like Prometheus?
A: A separate EXPORTER component would periodically read all registered metrics' current state (iterating the MetricsRegistry) and format them per Prometheus's expected text exposition format at a scrape endpoint (/actuator/prometheus, from Metrics & Monitoring) — the collection/storage layer (this exercise) and the export/format layer are cleanly separable concerns.
Q: Should Counter use a plain long with synchronization, or AtomicLong as shown?
A: AtomicLong is the correct choice for a counter incremented from many concurrent threads — it provides lock-free, thread-safe increments via compare-and-swap (the same underlying mechanism discussed in this course's concurrency-focused lessons), meaningfully cheaper under contention than a synchronized block around a plain long.