skip to content

You need to express a property of a type that adds no operations — 'instances may be moved between threads', 'this may be persisted'. Compare expressing it as an empty interface, as metadata such as an annotation or attribute, and as a compile-time predicate.

level: principalimportance: nice to knowfreq 28%

answer

  1. the fact is a predicate over types: declared, labelled or computed
  2. declared marker propagates by hierarchy, property depends on contents
  3. Rust Send/Sync auto-derived from fields, unsafe impl to assert
  4. C# marker interface can be a generic constraint; an attribute cannot
  5. Go: empty interface marks nothing; unexported method seals instead

basics

~20 s

An empty interface is checkable by the type system and usable as a generic bound, but only the type's author can declare it and every subtype inherits it whether it stays true or not. Annotations need reflection. Rust's Send and Sync show a third way: compiler-derived from the fields, with an unsafe opt-in.

solid answer

~50 s

The property is a **predicate over types**; the design question is who computes it and when. - **Java**: `Serializable` and `Cloneable` are the classic empty interfaces, and they are inherited by every subtype — so a subclass that adds a non-serializable field still satisfies the type check and fails at run time. The property is not really a subtyping relation, which is why modern designs reach for an annotation plus a processor. - **Rust**: `Send` and `Sync` are marker traits the compiler **auto-derives from a type's fields** and withdraws when any field lacks them, with `unsafe impl` to assert manually. Computed, so it cannot be inherited falsely. - **C++**: expressed as a compile-time predicate — `std::is_trivially_copyable`, concepts — a query over the type, so it works for types you do not own and for primitives. - **C#**: a marker interface can be a generic constraint (`where T : IMyMarker`); an attribute cannot. - **Go**: an empty interface is satisfied by everything, so marking uses an unexported method instead.

code

rust · 7 lines
rust
struct Job { data: Vec<u8> }              // all fields Send -> Job is Send
struct Handle { p: std::rc::Rc<u8> }      // Rc is not Send -> Handle is not

fn spawn<T: Send>(t: T) { /* ... */ }

spawn(Job { data: vec![] });   // ok
// spawn(Handle { .. });       // compile error, no annotation involved

go deeper

for a junior

Know what an empty marker type is and that it says something about a type rather than adding operations.

for a middle

Explain why inheritance is the wrong propagation rule for content-dependent properties, and name one language that computes the property instead of declaring it.

for a senior

Pick the encoding from enforcement needs: generic constraint versus analyzer versus derived predicate, including the third-party-type case.

for a principal

Set the convention for the codebase, insist that escape hatches be explicit and auditable, and reject markers for properties a new field can invalidate.

## The shape of the problem Some facts about a type add no operations: it is safe to move to another thread, it may be persisted, it is a legal cache key, its values are immutable. Each is a **predicate over types** — a yes/no fact — and languages offer three quite different places to put it. ## Option 1: the empty interface Declare a type with no members and have conforming types implement it. Java's `Serializable` and `Cloneable` are the canonical examples. What it buys: the property lives in the type system. The compiler can check it at a parameter position, and in C# it can serve as a **generic constraint** (`where T : IMyMarker`), which is something metadata simply cannot do. What it costs, and this is the interesting part: - **Only the author may declare it.** A third-party type that genuinely has the property cannot be marked, unless the language allows retroactive conformance. - **It is inherited, and inheritance is the wrong propagation rule for most such predicates.** A class implementing `Serializable` has a subclass that adds a socket or a thread field. The subtype still satisfies the marker — subtyping says so — and serialization fails at run time. The predicate depends on the type's *contents*, but the marker propagates by *hierarchy*. That mismatch is the core defect of the pattern. - **It clutters the type's declared identity** with facts that are not roles. ## Option 2: metadata (annotation or attribute) Attach a label rather than a supertype: a Java annotation, a C# attribute, a Python decorator that sets a flag. Metadata can be applied more precisely (fields, parameters, methods), can carry arguments, and can be defined with a retention or targeting policy. The cost is that plain metadata is invisible to the type checker. Enforcement moves either to **reflection at run time**, which is late and only covers paths you execute, or to a **compile-time processor or analyzer**, which is early but is extra machinery each project must run. Metadata also cannot appear in a generic constraint, so the compiler cannot reject a call site that supplies an unlabelled type. ## Option 3: a compile-time predicate Do not put the fact on the type at all; ask a question about it. **C++** does this pervasively: `std::is_trivially_copyable<T>`, `std::is_nothrow_move_constructible<T>`, and, since C++20, concepts that constrain templates on such predicates. Because it is a query, it works for types you did not write, for primitives, and for instantiations of templates — none of which can be made to implement your interface. **Rust** takes the strongest version. `Send` and `Sync` are *auto traits*: the compiler derives them structurally from a type's fields. A struct whose fields are all `Send` is `Send`; put an `Rc<T>` inside and it stops being `Send`, automatically, with no declaration anywhere. Where the derivation is too conservative — a type whose safety comes from an invariant the compiler cannot see — you write `unsafe impl Send for T`, which is both an escape hatch and an auditable marker of exactly where a human took responsibility. The property is therefore computed and revoked automatically, which is precisely what the Java marker cannot do. **Go**'s position is a useful negative case: an empty interface is satisfied by *every* type, so it cannot mark anything at all. Go instead achieves a related goal — sealing a set of implementations — with an interface containing an **unexported** method, which only types in the declaring package can supply. ## Choosing Ask three questions. 1. **Does the property follow from the type's structure?** If yes, a derived or computed predicate is right, because it will stay correct as the type changes — Rust's model. A declared marker will drift. 2. **Does the compiler need to enforce it at a call site?** If yes, you need something the type system can see: a marker interface as a generic constraint in C# or a concept in C++. Metadata will not do it without a custom analyzer. 3. **Must it apply to types you do not own?** If yes, rule out anything requiring the author's declaration; you need a predicate, a retroactive conformance mechanism, or a wrapper. A fourth consideration is auditability: `unsafe impl` in Rust and a suppression annotation elsewhere are valuable exactly because they are greppable. A design where the escape hatch is invisible — for example, silently inheriting a marker — is worse than one where it is loud. ## The pattern to avoid The failure mode is a marker interface for a property that depends on the type's contents rather than its role, applied in a hierarchy where subtypes add state. The type system says yes, the runtime says no, and the gap widens quietly with every subclass. If the property can be invalidated by adding a field, do not encode it as a supertype.

  • Why does a subclass that adds a non-serializable field still satisfy a serialization marker interface?
    Because conformance is inherited: subtyping says a subclass is everything its superclass is, and the marker is part of that. The property being marked, however, depends on the type's fields, not on its position in the hierarchy. The type check therefore stays green while the runtime behaviour becomes invalid — the mismatch that makes declared markers unreliable for structural properties.
  • What does `unsafe impl Send for T` mean, and why is it better than silently allowing the claim?
    It asserts to the compiler that the type upholds the invariant even though its fields do not prove it — typically because a raw pointer is protected by a lock the compiler cannot see. It is better than silence because the assertion is explicit, localized and greppable: a reviewer can enumerate every place a human overrode the derivation, which is impossible when a property is merely inherited.
  • When would you still choose a marker interface over an annotation in a language that has both?
    When the compiler must enforce it at a call site. C# lets you write `where T : IMyMarker` on a generic, so an unmarked type is rejected where it is used; an attribute cannot appear in a constraint and needs reflection or a custom analyzer. If the property is a genuine role the type plays and does not depend on fields a subtype might add, the marker is also the more honest modelling.

A declared marker is a badge someone pinned on once; a computed predicate is a turnstile that rechecks the holder's credentials every time. Adding a new field is exactly the moment the badge stops being true and the turnstile notices.

saying these in an interview costs you the question

  • Encoding a property that depends on a type's fields as an inherited marker, so subtypes claim it falsely.
  • Believing a Go empty interface can mark anything — every type satisfies it.
  • Assuming an annotation or attribute is checked by the compiler without a processor or analyzer.
  • Thinking Rust's Send and Sync are declared by the programmer rather than derived from the type's fields.
  • Reaching for reflection-based enforcement when a generic constraint or a compile-time predicate would reject the bad call site outright.

context