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