Why architecture matters here
Metaprogramming matters where the alternative is either boilerplate or runtime reflection, and both are bad. Consider deriving a JSON encoder for a case class: without metaprogramming you either hand-write an encoder per class (boilerplate that drifts from the class definition) or use runtime reflection (slow, unsafe, breaks on obfuscation, invisible to the compiler). Compile-time derivation gives you the best of both: the encoder is generated from the class structure (no boilerplate, never drifts) and generated as typed compiled code (as fast as hand-written, checked by the compiler, no runtime reflection). This is why every serious Scala library — circe, tapir, the whole Typelevel/ZIO ecosystems — uses metaprogramming for derivation: it's the only way to get zero-boilerplate, zero-runtime-cost, type-safe generation.
The reason the Scala 3 redesign matters — beyond safety — is that the layered design lets you use the least powerful tool that works. Most 'metaprogramming' needs don't need macros at all: inline handles compile-time constants, guaranteed inlining of hot functions, and compile-time branching (inline match); Mirror-based derivation handles most type-class generation declaratively (derives Codec) without writing a macro. Full macros (quotes + splices + reflection) are reserved for the genuinely hard cases — inspecting arbitrary code, generating complex specialized implementations, building DSLs. The discipline of reaching for inline first, derivation second, and macros only when necessary is what keeps metaprogramming a tool rather than a liability, because each level up costs build time and debuggability.
And the honest architectural truth is that metaprogramming has real costs that must justify its benefits. Macros run at compile time, so heavy macro use slows builds (the derivation of a hundred complex codecs is compile work every build); generated code is harder to debug (you're debugging code you didn't write, sometimes needing -Xprint to see it); and macro code is harder to write and maintain than ordinary code (a different skill, a smaller pool of people who can fix it). The libraries that use metaprogramming well pay these costs for genuine, high-leverage wins (eliminating boilerplate across thousands of use sites); application code that reaches for macros to solve problems inline or a helper function would handle pays the costs for no real benefit. Knowing the difference is the core judgment.
The architecture: every piece explained
Top row: the inline layer and the quote world. inline def guarantees the function body is inlined at each call site (not a hint — a guarantee), enabling compile-time evaluation and eliminating call overhead; combined with inline parameters, it specializes code per call. inline if / inline match branch at compile time — the condition is evaluated during compilation and only the taken branch survives, enabling type-driven code selection without runtime cost. Quotes '{ } lift code into values: '{ 1 + 1 } is an Expr[Int] — a typed representation of the code, manipulable in macro implementations. Splices ${ } do the inverse — insert an Expr[T] into surrounding code: inside a quote, ${ expr } splices generated code in. The quote/splice duality (build typed code, insert it) is the core of principled macro construction, and because quotes are typed, the compiler checks the code you generate.
Middle row: macros and reflection. Macros combine the layers: an inline def is the entry point (so calls are inlined and the macro runs), and its body calls a macro implementation via ${ macroImpl(...) } — a method taking and returning Exprs, running at compile time, using quotes/splices to build the generated code. Mirror is structural reflection: for case classes and enums, the compiler provides a Mirror describing the structure (field names, types, the ADT's cases) at compile time, enabling derivation without full macro machinery — you iterate the Mirror's structure to build the instance. TASTy inspection gives macros access to the typed AST (the reflection API — quotes.reflect) for the hard cases needing to inspect arbitrary code structure. Derivation is the high-level application: a type class's derived given (using Mirror or macros) generates instances, invoked by derives.
Bottom rows: the safety and the judgment. Phase consistency is the principle keeping compile-time and runtime straight: a value from the compile-time phase (a macro's parameter) can't be used directly at runtime and vice versa — the Expr/plain-value distinction enforces it, preventing the phase-confusion bugs that plagued Scala 2. Hygiene and typing: generated code is hygienic (identifiers don't accidentally capture or collide with surrounding scope) and typed (checked by the compiler), so macros can't silently generate broken or capturing code — the safety-by-construction that makes Scala 3 macros trustworthy. The ops strip is the judgment layer: build-time cost (metaprogramming is compile work — heavy use slows builds, measure it), debuggability (generated code needs -Xprint:staging or -Xprint:typer to inspect — plan for debugging), and when NOT to metaprogram (the most important discipline — inline over macro, derivation over hand-macro, helper function over any of it when possible).
End-to-end flow
Walk the layered choice through a real need: a library wants zero-boilerplate JSON codecs. The first reach is Mirror-based derivation, not macros: the JsonCodec type class defines a derived given that takes a Mirror.Of[T], iterates the mirror's element types and labels at compile time, and assembles the codec from the codecs of the fields (recursively, via the field types' own given codecs). case class User(name: String, age: Int) derives JsonCodec works: the derivation read User's structure from its Mirror and generated a codec composing String's and Int's codecs — no macro written, declarative, typed, and the generated code is as fast as hand-written. For 95% of derivation needs, Mirror is enough, and the library never touches the reflection API.
The genuine macro case arises for the hard 5%: the library also wants a @jsonField("custom_name") annotation that renames fields in the JSON. Mirror alone doesn't expose annotations, so this needs the reflection API: a macro inspects the class's field annotations via quotes.reflect, reads the custom names, and generates a codec using them — quotes to build the encoding code, splices to insert the field-name logic, all typechecked. This is metaprogramming earning its cost: the annotation-driven renaming is impossible without inspecting the typed AST, the macro does it once, and thousands of use sites benefit. The library author accepts the debugging cost (when the macro misbehaves, they use -Xprint:staging to see the generated code) because the leverage justifies it.
The anti-pattern vignette teaches the discipline. An application developer, impressed by the library's macros, reaches for a macro to 'generate' some repetitive validation logic in their app — three similar validation functions they could have written as one higher-order function. The macro works but costs: build times tick up (the macro runs every compile), the next developer can't understand the generated validation (they're debugging code nobody wrote), and a bug in the macro is far harder to fix than a bug in a plain function. Code review catches it: the reviewer points out that an inline def would handle the compile-time part they actually needed, and a plain higher-order function handles the rest — no macro, no build cost, no debuggability hit. The consolidated lesson the team codifies: reach for the least powerful tool — plain function, then inline, then derivation, then macro — and only pay metaprogramming's costs (build time, debuggability, maintenance) when the leverage (boilerplate eliminated across many sites, or generation genuinely impossible otherwise) clearly justifies them. Metaprogramming is a library-author's power tool, rarely an application developer's, and the Scala 3 layering makes it easy to stop at the right level.