Why architecture matters here
The reason implicit resolution is worth mastering is that it moves work from runtime to compile time and from the caller to the library author, safely. Type classes let you add behavior to a type you don't own — an Ordering for a third-party class, a JSON codec for a case class — without inheritance and without touching the original definition. The compiler proves at build time that the required capability exists, so 'can this be sorted?' or 'can this be serialized?' becomes a compile error rather than a runtime exception. That is a large safety win, but it is only a win if the resolution is predictable; an unpredictable search that sometimes picks the wrong instance is worse than an explicit argument.
The core of the mechanism is that resolution is driven by types, not names. When the compiler needs a using ord: Ordering[User], it does not look for something called ord; it looks for any value whose type is Ordering[User], wherever the rules say it may look. This is why a single import can change program behavior, why the same call can resolve differently in two files, and why moving a given into a companion object makes it available everywhere without an import. The scoping rules are the whole ballgame, and they are specifically designed so that the 'canonical' instance for a type — the one in its companion object — is always found, while ad-hoc local overrides require a deliberate import.
The recursive dimension is what elevates this from convenience to architecture. Given a given for Encoder[Int] and a rule for Encoder[List[A]] that needs an Encoder[A], the compiler can build an Encoder[List[Int]] by composing them, and an Encoder[Map[String, List[Int]]] by composing further. Derivation for case classes generalizes this: an instance for a product type is assembled from instances for its fields. The upside is enormous — you write instances for primitives and the compiler constructs the rest. The downside is that this search can go deep, can diverge if a type refers to itself carelessly, and can produce errors that describe the innermost failure rather than the top-level one you care about.
So the trade-off is legibility and build cost against expressiveness. Every implicit you make available is a candidate the compiler must consider and a source of potential ambiguity; every derivation you enable is compile time spent. The architectural discipline is to make the common instance obvious and unambiguous (companion object), keep alternative instances behind explicit imports, and treat the resolution rules as an interface you are designing against rather than an accident you tolerate.
The architecture: every piece explained
Top row: the search itself. Everything starts from the expected type — the type of the missing using/implicit parameter, fully computed by the time resolution runs. The compiler searches two broad regions in order. First the local scope: given/implicit definitions in the current lexical scope, explicit using parameters in enclosing methods, and anything brought in by import. If the local scope produces a unique answer, the search stops there. Otherwise it consults the implicit scope of the target type: the companion objects of the type and of the types that appear in it (its type arguments, its supertypes). This two-phase design is deliberate — local definitions and imports win, so a caller can override, while the type's own companion supplies the default that needs no import. Priority and specificity rules then rank candidates when more than one is eligible.
Middle row: type classes and recursion. A type class is a trait parameterized by a type; its given instances are the values resolution finds. The recursive search is the powerful part: an instance definition may itself take using parameters, so resolving Show[List[User]] triggers resolution of Show[User], which for a case class may trigger resolution of a Show for each field. Context bounds (def f[A: Ordering]) are sugar for a using parameter and the ordinary way methods declare 'I need a type class instance for A.' Implicit conversions live here too but are deliberately narrow and discouraged in Scala 3 (they must be an explicit Conversion given, opt-in via an import) precisely because unconstrained conversions historically made resolution unpredictable.
Bottom rows: the guardrails. Divergence checks stop the recursive search from looping forever when a type refers to itself in a way that would generate an infinite chain of subgoals; the compiler detects the growing pattern and reports divergence rather than hanging. Coherence — the informal expectation that there is one canonical instance per type — is what companion-object placement is designed to preserve. When two equally specific candidates both match, the compiler raises an ambiguity error rather than silently choosing, because silently choosing would make behavior depend on import order. The ops strip names the practical consequences that fall out of all this: derivation adds real compile-time cost, imports must be kept disciplined so scopes stay predictable, error messages need help to point at the right subgoal, and deep automatic derivation has limits you design around with semi-automatic derivation.
End-to-end flow
Follow one resolution. You write a function def render[A: Show](a: A): String = summon[Show[A]].show(a) and call render(order) where order is a case class Order(id: Long, lines: List[Item]). The compiler needs a Show[Order].
It searches the local scope first: are there any given Show[Order] definitions in scope, or imported? Suppose not. It then consults the implicit scope of Show[Order] — the companion objects of Show and of Order. In the Show companion sits a given that can derive a Show for any product type, itself taking using instances for each field's type. The compiler selects it and now has new subgoals: Show[Long] and Show[List[Item]]. Show[Long] resolves directly to a primitive instance in the companion. Show[List[Item]] matches a given Show[List[A]] that needs a Show[Item], which recurses into Item's fields. The whole tree resolves, and the compiler synthesizes a concrete Show[Order] assembled from the pieces — no runtime reflection, all of it inlined at compile time.
Notice what carried the whole derivation: the expected type at each step. The compiler never searched for a name — it searched for a value whose type was exactly the subgoal it currently needed, and each match either resolved directly or introduced strictly smaller subgoals (Show[Order] needs Show[Long] and Show[List[Item]], which need Show[Item], which needs instances for its fields). That shrinking is what guarantees the search terminates, and it is also why a single missing instance anywhere in the tree fails the whole request: there is no partial answer, because the top-level Show[Order] literally cannot be assembled without every piece. The expressiveness and the fragility come from the same property — the resolution either succeeds completely or it succeeds not at all.
Now change one thing: a developer adds import mymodule.given at the top of the file, and that module also defines a given Show[Long] that formats numbers with grouping separators. Suddenly there are two Show[Long] candidates visible for the subgoal — the imported one in local scope and the primitive one in the companion. Local scope wins over implicit scope, so the imported one is chosen, and every Long in every derived Show now renders with separators. Nothing broke; behavior shifted because a scope changed. This is exactly the property that makes implicits powerful and the property that makes undisciplined imports dangerous.
The stress case is ambiguity. Suppose two libraries you both import each export a top-level given Show[Order] at equal priority. Now the local scope contains two equally specific candidates for the top-level goal, and neither the priority nor the specificity rules break the tie. The compiler refuses to guess: it emits an ambiguity error naming both candidates. The fix is architectural, not a compiler flag — import only one, or introduce a more specific given that the specificity rules prefer, or wrap the type so its canonical instance lives in a companion you control. And when instead the failure is a missing instance three levels deep — say Item contains a field of a type with no Show — the raw error points at that innermost subgoal, which is why teams add compiler options and annotations that make the message report the top-level goal that a human actually asked for.