skip to content

As a principal engineer, when would you prefer declarative @Timed/@Counted over programmatic Micrometer instrumentation, and what are the risks at scale?

level: principalimportance: nice to knowfreq 22%

answer

  1. declarative = breadth/low boilerplate; imperative = dynamic tags/partial timing
  2. extraTags is STATIC — no runtime tag values
  3. cardinality: never tag unbounded values; watch auto exception tag
  4. silent no-ops = empty dashboards, not errors
  5. govern with MeterFilter + naming conventions

basics

~20 s

Use @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 s

Declarative @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
java
// 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

for a junior

Know both an annotation-based and a code-based way to add metrics exist.

for a middle

State that extraTags are static and imperative allows dynamic tags/partial timing.

for a senior

Weigh proxy limits, dynamic-tag needs, and basic cardinality when choosing.

for a principal

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.

context