You're designing a small set of marker annotations for your codebase. What principles guide declaring them, and what mistakes turn an annotation into a poor abstraction?
answer
- public, bodiless, single-purpose by design
- name for intent; one meaning each
- always pair with a consumer — no dead metadata
- use interfaces/sealed/enums for compile-time contracts
- markers are public API surface — hard to remove later
basics
~20 sKeep each annotation a small, clearly named public marker that means one thing, and make sure something actually reads it. Avoid using an annotation when a normal type, interface, or function would express the idea better.
solid answer
~50 sGood marker-annotation design starts with the shape Kotlin gives you: `annotation class Name` — implicitly public, bodiless, single-purpose. Principles: name it for intent (`@Internal`, `@Audited`), keep one annotation to one meaning, and always pair a declared annotation with a real **consumer** (processor, framework, or reflection) — an unread annotation is dead metadata. Prefer annotations for *cross-cutting metadata* that tooling reads, not for expressing behavior or contracts better served by interfaces, sealed types, or functions. Anti-patterns: declaring annotations nobody consumes; overloading one annotation with vague meaning; using an annotation where a type/interface would give compile-time guarantees; or treating application as if it injects behavior. Because the type is public by design, also consider its API surface — a leaked, never-removed marker becomes permanent debt. The minimal, public, no-body shape is a feature: it forces annotations to stay metadata-only.
go deeper
Can declare a marker annotation but may not weigh design tradeoffs.
Knows annotations need a consumer and should be named clearly and kept single-purpose.
Articulates when to choose annotations vs interfaces/sealed types and names concrete anti-patterns.
Treats markers as public API surface, reasons about lifecycle/removal cost, and frames the metadata-vs-contract tradeoff at codebase scale.
## Start from what the shape gives you `annotation class Name` is **public, bodiless, single-purpose** by construction. Good design leans into that: an annotation should be a *small, clear marker*, not a Swiss-army type. ## Principles for declaring marker annotations - **Name for intent, not mechanism.** `@Audited`, `@Internal`, `@FeatureFlagged` read as *what they mean*. PascalCase like any type. - **One annotation, one meaning.** Don't let `@Special` mean three different things in three places. - **Always have a consumer.** A declared `annotation class` with no reader is dead metadata. Pair it with the compiler, a KSP/KAPT processor, a framework scan, or your own reflection from day one. - **Keep it public-minimal.** Since it's public by default, treat each marker as part of your API surface; don't proliferate markers you can't remove later. - **Prefer the inert-marker model.** Application attaches metadata; the *consumer* supplies behavior. Keep that separation clean. ```kotlin annotation class Audited // clear, single meaning, public, no body @Audited fun transferFunds() { /* a scanner/aspect reads @Audited to log */ } ``` ## When NOT to use an annotation Annotations are weak at expressing **contracts** the compiler should enforce: - If you need a method to exist, use an **interface**, not `@MustImplement`. - If you need a closed set of variants, use a **sealed** hierarchy or **enum**, not marker annotations. - If you need behavior, write a **function** or use composition — not an annotation that secretly implies logic. Annotations give *runtime/tooling-time* metadata; types give *compile-time* guarantees. Choose by which guarantee you need. ## Common mistakes that make a poor abstraction - **Unconsumed markers** — declared but nothing reads them, so they mislead readers into thinking something happens. - **Vague/overloaded markers** — one annotation meaning many things kills clarity. - **Annotation-as-behavior** — assuming `@Loggable` logs by itself instead of providing a consumer. - **Reaching for annotations where a type fits** — losing compile-time safety for no reason. - **Forgetting the public surface** — markers leak across modules and become permanent because removing them breaks consumers. ## Takeaway The minimal public no-body shape is a constraint that *helps*: it nudges annotations toward being clean, single-purpose metadata markers. Design within that grain — clear name, one meaning, a real consumer — and reach for ordinary types when you need behavior or compile-time contracts.
- When would an interface beat a marker annotation?When you need the compiler to enforce that implementers provide members or fit a closed type set — annotations give no compile-time contract, interfaces and sealed types do.
- Why is an unconsumed annotation a design smell?It implies something acts on it when nothing does, misleading readers and adding permanent public surface for zero benefit.
A marker annotation is like a department stamp on a form: useful only because a clerk downstream is trained to act on that exact stamp — and useless or confusing if no one is.
saying these in an interview costs you the question
- Designing annotations with no consumer and assuming they 'do something'
- Using a marker annotation where an interface or sealed type gives real compile-time safety
- Overloading one annotation with multiple vague meanings
- Ignoring that public-by-default markers become hard-to-remove API surface
- Treating application as behavior injection