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.