A type class is a programming pattern that defines behavior for a type without requiring the type to inherit from a particular base class. In Scala 3, the pattern commonly uses a trait, given instances, using clauses, and extension methods.
Define the capability
trait Show[A]:
def show(value: A): String
given Show[Int] with
def show(value: Int): String = value.toString
def render[A](value: A)(using instance: Show[A]): String =
instance.show(value)
The compiler searches for an eligible contextual instance at the call site. This is compile-time resolution, not runtime reflection or automatic proof that an instance is semantically correct.
Add ergonomic syntax
extension [A](value: A)
def shown(using instance: Show[A]): String = instance.show(value)
val text = 42.shown
Extension methods improve call-site syntax but do not modify the original type. Keep names discoverable and avoid imports that make behavior difficult to trace.
Use context bounds and summon
def renderAll[A: Show](values: List[A]): List[String] =
val instance = summon[Show[A]]
values.map(instance.show)
A context bound expresses that a contextual instance is required. Exact desugaring details can evolve; follow documentation for the deployed Scala compiler.
Compose instances carefully
given [A](using showA: Show[A]): Show[List[A]] with
def show(values: List[A]): String =
values.map(showA.show).mkString("[", ", ", "]")
Generic derivation can reduce repetition, but overlapping or surprising instances harm coherence. Put canonical instances in predictable scopes, require explicit imports for alternatives, and avoid broad conversions.
Model a discount rule without hidden state
trait Discount[A]:
def amount(value: A): BigDecimal
final case class Order(total: BigDecimal)
given Discount[Order] with
def amount(order: Order): BigDecimal =
if order.total >= 100 then order.total * BigDecimal("0.10")
else BigDecimal(0)
def finalTotal[A](value: A, total: BigDecimal)(using d: Discount[A]) =
total - d.amount(value)
This illustrates dispatch only. Production money code also needs currency, rounding, validation, rule version, effective dates, stacking policy, audit evidence, and tests.
Test laws and resolution
- Test each instance with representative and boundary values.
- Test algebraic laws when the abstraction claims them.
- Compile examples that prove the intended instance resolves.
- Test that ambiguous imports fail or require explicit selection.
- Avoid global mutable state in contextual instances.
Migrate from Scala 2 deliberately
Scala 2 commonly used implicit val, implicit def, and implicit classes. Scala 3 supports migration paths while recommending given, using, and extension methods. Conversion rules, implicit search, imports, and compiler diagnostics differ; use the official migration guide and pin compiler/library versions.
For larger applications, connect the pattern to Scala in data-intensive systems, typed failure handling in Scala error handling, and interoperability constraints in Scala and Spark.
Originally published November 15, 2018; technically reviewed and substantially updated September 4, 2026.

Historical comments from Datanizant
No public comments on this article
No approved public comments were included in the WordPress export for this article.