Best Practices & Design Patterns
45.1 Idiomatic Scala
Prefer val, immutable collections, and pure functions.
Model the domain with case classes + enums/sealed traits; make illegal states unrepresentable.
Use Option/Either instead of null and exceptions for expected failures.
Annotate public method return types.
Keep implicits/givens explicit, local, and few; avoid implicit conversions.
Use pattern matching on sealed types for exhaustiveness.
Favor composition (traits, functions) over deep inheritance.
Pick one style (FP with effects, or simple direct style) per codebase and be consistent.
Format with scalafmt, lint with scalafix and WartRemover, enable strict compiler flags (-Wunused:all, -Werror in CI, or sbt-tpolecat).
Use opaque types for domain primitives.
45.2 Classic design patterns in Scala
companion apply function values / type classes stackable traits, extension methods pattern matching on ADTs
copy on case classes, phantom-typed builders constructor params, givens, ZLayer, MacWire given conversions / type class instances streams (fs2), actors, callbacks Command / Interpreter
ADTs + interpreters, free monads, tagless final
45.3 Functional architecture
Functional core, imperative shell: pure domain logic, effects at the edges.
Ports & adapters (hexagonal) with traits as ports.
Tagless final for testable services.
Event sourcing / CQRS with Pekko Persistence or edomata/zio-entity.
45.4 Common pitfalls
Await.result everywhere compose futures; block only at the edge Using return in lambdas expression-oriented code, boundary .get on Option/Try fold, getOrElse, pattern matching explicit parameters, scoped givens Mutable state shared across threads immutable data, Ref, actors
Huge for-comprehensions mixing effect types monad transformers or one effect type Blocking inside a compute thread pool IO.blocking, blocking {}
Ignoring variance compile errors with casts redesign with bounds