| name | scala-patterns |
| description | Idiomatic Scala patterns: ADTs with sealed traits and case classes, typeclass pattern, Option/Either/Try error handling, for-comprehensions, Cats Effect (IO, Resource, Ref, Fiber), ZIO fundamentals, Scala 3 features (given/using, enums, extension methods, opaque types, union types). Covers both Scala 2.13 and Scala 3. Use when writing Scala, reviewing Scala code, or designing Scala domain models. |
Scala Patterns
When to Activate
- Writing any Scala source (
.scala, .sc, build.sbt)
- Designing domain models or error handling strategies
- Choosing between Future, Cats Effect IO, and ZIO
- Migrating Scala 2 code to Scala 3
- Reviewing Scala code for idiomatic patterns, typeclass usage, or ADT completeness
- Adding concurrency to a service using Cats Effect
Ref, Fiber, or Resource
- Encountering compile errors related to implicit resolution,
given/using, or type inference
ADTs — Algebraic Data Types
Model domain states exhaustively. Compiler enforces completeness.
Scala 2 — Sealed Trait + Case Classes
sealed trait PaymentResult
case class Success(transactionId: String, amount: BigDecimal) extends PaymentResult
case class Declined(reason: String) extends PaymentResult
case class Error(cause: Throwable) extends PaymentResult
// Exhaustive pattern match — compiler warns on missing cases
def describe(result: PaymentResult): String = result match
case Success(id, amount) => s"Paid $amount (tx: $id)"
case Declined(reason) => s"Declined: $reason"
case Error(cause) => s"Error: ${cause.getMessage}"
Scala 3 — Enum (preferred)
enum PaymentResult:
case Success(transactionId: String, amount: BigDecimal)
case Declined(reason: String)
case Error(cause: Throwable)
Option / Either / Try
Option — presence or absence
def findUser(id: UserId): Option[User] = users.get(id)
// Chain without null checks
val greeting: String =
findUser(id)
.map(u => s"Hello, ${u.name}")
.getOrElse("Hello, stranger")
// Fail fast with toRight
val user: Either[NotFound, User] =
findUser(id).toRight(NotFound(id))
Either — expected failures with typed errors
sealed trait AppError
case class NotFound(id: String) extends AppError
case class ValidationError(msg: String) extends AppError
def validateAge(n: Int): Either[ValidationError, Int] =
if n >= 0 && n <= 150 then Right(n)
else Left(ValidationError(s"Invalid age: $n"))
// Chain with for-comprehension
val result: Either[AppError, Profile] =
for
user <- findUser(id).toRight(NotFound(id.value))
age <- validateAge(user.rawAge)
profile <- buildProfile(user, age)
yield profile
Try — wrapping exceptions from Java/legacy code
import scala.util.{Try, Success, Failure}
def readConfig(path: String): Try[Config] =
Try(ConfigFactory.parseFile(new File(path)))
// Convert to Either for composition
readConfig("app.conf")
.toEither
.left.map(e => ConfigError(e.getMessage))
Typeclass Pattern
Separate behavior from data. Enables ad-hoc polymorphism.
// 1. Define the typeclass trait
trait JsonEncoder[A]:
def encode(value: A): Json
// 2. Instances in companion object
object JsonEncoder:
given JsonEncoder[String] = Json.Str(_)
given JsonEncoder[Int] = Json.Num(_)
given [A: JsonEncoder] JsonEncoder[List[A]] =
xs => Json.Arr(xs.map(summon[JsonEncoder[A]].encode))
// 3. Syntax extension
extension [A: JsonEncoder](value: A)
def toJson: Json = summon[JsonEncoder[A]].encode(value)
// Usage — no explicit import needed if instances are in scope
42.toJson // Json.Num(42)
List("a").toJson // Json.Arr(...)
For-Comprehensions
Use for-comprehensions instead of nested flatMap chains:
// Avoid — nested, hard to read
fetchUser(id)
.flatMap(u => fetchOrders(u.id)
.flatMap(orders => computeTotal(orders)
.map(total => Invoice(u, total))))
// Preferred
for
user <- fetchUser(id)
orders <- fetchOrders(user.id)
total <- computeTotal(orders)
yield Invoice(user, total)
For-comprehensions work with any monadic type: Future, IO, Either, Option.
Cats Effect
IO — pure effect type
import cats.effect.IO
def readFile(path: String): IO[String] =
IO.blocking(scala.io.Source.fromFile(path).mkString)
def writeFile(path: String, content: String): IO[Unit] =
IO.blocking(Files.write(Paths.get(path), content.getBytes))
// Compose
val program: IO[Unit] =
for
content <- readFile("input.txt")
processed <- IO(process(content))
_ <- writeFile("output.txt", processed)
yield ()
Resource — safe acquisition and release
import cats.effect.Resource
def dbPool(config: DbConfig): Resource[IO, HikariDataSource] =
Resource.make(
IO(new HikariDataSource(config))
)(pool => IO(pool.close()))
// Use and automatically release
dbPool(config).use { pool =>
IO(pool.getConnection()).flatMap(doWork)
}
Ref — concurrent mutable state
import cats.effect.Ref
def counter: IO[Unit] =
for
ref <- Ref.of[IO, Int](0)
_ <- (ref.update(_ + 1)).replicateA(100).parSequence
n <- ref.get
_ <- IO.println(s"Count: $n")
yield ()
Blocking vs compute
// Blocking I/O — runs on dedicated blocking thread pool
IO.blocking(Files.readAllBytes(path))
// CPU-bound — runs on compute pool
IO(computeHash(data))
// Never: Thread.sleep, blocking calls on compute pool
ZIO Basics
import zio.*
// Define effect with environment, error, and result types
def findUser(id: UserId): ZIO[UserRepo, DatabaseError, Option[User]] =
ZIO.serviceWithZIO[UserRepo](_.find(id))
// Provide dependencies via layers
val live: ZLayer[Any, Nothing, UserRepo] =
ZLayer.succeed(PostgresUserRepo(pool))
val program = findUser(id).provide(live)
Scala 3 — Key Features
Opaque Types — zero-cost domain wrappers
opaque type UserId = String
object UserId:
def apply(s: String): UserId = s
extension (id: UserId)
def value: String = id
def show: String = s"UserId($id)"
Extension Methods
extension (s: String)
def toSlug: String =
s.toLowerCase.replaceAll("[^a-z0-9]+", "-").trim('-')
extension [A](opt: Option[A])
def orFail(msg: String): Either[String, A] =
opt.toRight(msg)
Given / Using (Scala 3 implicits)
given Ordering[User] = Ordering.by(_.name)
def sortUsers(users: List[User])(using ord: Ordering[User]): List[User] =
users.sorted
// Compiler resolves `given Ordering[User]` automatically
sortUsers(allUsers)
Union Types
type StringOrInt = String | Int
def format(value: StringOrInt): String = value match
case s: String => s
case n: Int => n.toString
Anti-Patterns
| Anti-Pattern | Better Alternative |
|---|
null as return value | Option[A] |
throw for business errors | Either[Error, A] |
var for accumulation | fold, scan, or immutable builders |
Future for everything | Cats Effect IO or ZIO for control |
| Java-style loops | map, flatMap, foldLeft |
Nested flatMap (>2 levels) | For-comprehension |
| Blocking on compute pool | IO.blocking / ZIO.blocking |