As a principal engineer, when would you prefer declarative @Timed/@Counted over programmatic Micrometer instrumentation, and what are the risks at scale?
answer
- declarative = breadth/low boilerplate; imperative = dynamic tags/partial timing
- extraTags is STATIC — no runtime tag values
- cardinality: never tag unbounded values; watch auto exception tag
- silent no-ops = empty dashboards, not errors
- govern with MeterFilter + naming conventions
basics
~20 sUse @Timed/@Counted for quick, low-boilerplate, method-level metrics on Spring beans. Use programmatic Micrometer when you need dynamic tags, to time part of a method, or to avoid proxy limits. At scale watch tag cardinality, the AOP proxy constraints, and consistent naming.
solid answer
~50 sDeclarative @Timed/@Counted shine for coarse, method-level observability with minimal boilerplate — annotate a service method and get a tagged Timer/Counter. They're great for broad, uniform coverage and readable code. But they're constrained: tags are **static** (extraTags is fixed at compile time), you can only instrument a whole method (not a sub-section), and you inherit **Spring AOP proxy limitations** (self-invocation, public/non-final, bean-only). Programmatic instrumentation with an injected `MeterRegistry`/`Timer.Sample` gives dynamic tag values, sub-method timing, and no proxy dependency — at the cost of boilerplate. At scale the real risks are: **tag cardinality explosion** (avoid user IDs/URLs as tags; the auto `exception` tag can also blow up), **too many histogram/percentile series**, **silent no-ops** from missing aspects or self-calls, and **naming inconsistency** across teams. I'd standardize naming conventions, cap cardinality via MeterFilters, and prefer declarative for uniform coverage while using imperative for hot, high-cardinality, or partial-method paths.
code
java · 29 lines// Programmatic when a tag value is dynamic (declarative extraTags can't do this)
@Service
public class OrderService {
private final MeterRegistry registry;
OrderService(MeterRegistry registry) { this.registry = registry; }
public Order place(Order req, Customer c) {
Timer.Sample sample = Timer.start(registry);
String outcome = "success";
try {
return doPlace(req);
} catch (RuntimeException e) {
outcome = "error";
throw e;
} finally {
sample.stop(Timer.builder("orders.place")
.tag("tier", c.tier()) // runtime-computed tag
.tag("outcome", outcome)
.register(registry));
}
}
}
// Example cardinality guard registered app-wide
@Bean
MeterFilter limitTags() {
return MeterFilter.maximumAllowableTags(
"orders.place", "tier", 20, MeterFilter.deny());
}go deeper
Know both an annotation-based and a code-based way to add metrics exist.
State that extraTags are static and imperative allows dynamic tags/partial timing.
Weigh proxy limits, dynamic-tag needs, and basic cardinality when choosing.
Set fleet-wide policy: naming standards, MeterFilter cardinality caps, smoke tests for meter existence, and a breadth-vs-precision instrumentation strategy.
## Two instrumentation styles **Declarative** (`@Timed`/`@Counted`): annotate the method; `TimedAspect`/`CountedAspect` do the work. Pros: minimal code, readable intent, uniform across many methods, easy to roll out via a class-level `@Timed`. **Programmatic**: inject `MeterRegistry`, build meters explicitly. ```java Timer.Sample sample = Timer.start(registry); try { doWork(); } finally { sample.stop(Timer.builder("orders.place") .tag("tier", customer.tier()) // dynamic tag value .register(registry)); } ``` Pros: **dynamic tag values**, **partial-method** timing, **no proxy dependency**, full control over percentiles/SLOs per call. ## When declarative is the right call - Broad, uniform method-level coverage of service/repository layers. - Team prefers low boilerplate and readable annotations. - Tags are static/known at build time (class/method/exception plus a couple of constant extraTags). - The methods are proper public bean methods invoked externally. ## When to go programmatic - You need a **tag value computed at runtime** (customer tier, outcome category, feature flag) — `extraTags` can't do this. - You must time **only a portion** of a method, or multiple named phases. - The code isn't a Spring bean, or is subject to self-invocation you can't refactor. - You want fine-grained control of distribution config per meter. ## Risks at scale (the principal-level substance) 1. **Cardinality explosion.** Each unique tag-value combination is a separate time series. Never tag with unbounded values (user IDs, request paths, emails). Note the **auto `exception` tag**: a method throwing many distinct exception types multiplies series. Bound it with a `MeterFilter` (`denyNameStartsWith`, `maximumAllowableTags`, or tag transforms). 2. **Histogram/percentile cost.** `histogram = true` adds many `_bucket` series; combined with `extraTags` this multiplies fast. Budget it; use SLO-bounded buckets. 3. **Silent failures.** Missing `TimedAspect` (no AOP) or self-invocation yields no metric and no error — dangerous because dashboards look empty, not broken. Establish smoke tests / assertions that key meters exist. 4. **Naming drift.** Without conventions, teams produce `orderPlace`, `order.place.time`, `place_order` — un-joinable dashboards. Enforce a naming standard (dot-delimited, base unit seconds) and common tag keys, ideally via `MeterFilter` renaming and code review. 5. **Proxy semantics leaking into design.** Relying on annotations can push people to awkward class layouts to avoid self-invocation. Sometimes imperative code is simpler than fighting the proxy. 6. **Overhead.** Each annotated call adds an AOP layer + meter lookup. Usually negligible, but on ultra-hot inner-loop methods prefer imperative or sampling. ## Governance recommendation - Default to declarative for uniform service-layer coverage. - Provide a small set of approved `MeterFilter` beans (cardinality caps, common tags like `application`, `region`). - Reserve programmatic instrumentation for dynamic-tag / partial-method / hot paths. - Treat metric names/tags as an API: reviewed, documented, versioned. The mature answer isn't 'annotations vs code' but 'annotations for breadth, code for precision, governed by cardinality and naming policy.'
- The @Timed exception tag is causing a cardinality spike. How do you contain it without losing all error visibility?Apply a MeterFilter that maps/normalizes exception values (e.g., collapse rare types into 'other' or cap distinct values via maximumAllowableTags), or record a coarser success/failure result tag instead of raw exception class names, keeping enough signal for alerting.
- Your dashboards are silently empty for a newly annotated method. How do you make this failure mode loud in the future?Add a startup/integration check that asserts expected meters exist in the MeterRegistry after exercising the path, and verify the aspect beans and AOP are present. Treat missing key meters as a build/deploy failure rather than discovering it in production.
saying these in an interview costs you the question
- Claiming @Timed extraTags can carry per-request dynamic values.
- Ignoring cardinality — proposing user IDs or URLs as tags.
- Treating declarative vs imperative as strictly either/or rather than complementary.
- Overlooking that missing metrics fail silently and look like empty (not broken) dashboards.