Scala has a well-known problem: it can be written in many styles, from Java-with-semicolons to pure functional with category theory abstractions. AI assistants without explicit guidance default to the most common patterns in their training data — which is often Java-style Scala from older codebases.
The result: mutable variables, null checks, raw Future.map chains without proper error handling, and classes where case classes belong.
A CLAUDE.md file tells the AI which Scala you're writing. Here are the 13 rules that matter most.
Rule 1: Scala version and style target
Scala version: 3.x (Scala 3 syntax — no Scala 2 `implicit` keyword, use `given`/`using`).
Style: functional-first. Immutable by default. Effects tracked explicitly.
Framework: [Akka / ZIO / Cats Effect / Play — specify yours]
Build: sbt with explicit dependency versions pinned in build.sbt.
Scala 3 changed the syntax for implicits, extension methods, and type classes significantly. AI trained on pre-Scala 3 material generates Scala 2 syntax. Make the version explicit.
Rule 2: Case classes for data — not plain classes
Use case classes for all data transfer objects, domain models, and value objects:
`case class User(id: UserId, name: String, email: Email)`
Benefits the AI must leverage:
- Automatic `equals`, `hashCode`, `copy`, `toString`
- Pattern matching support
- Immutability by default
- `unapply` for destructuring
Use `@derive` (Scala 3) for additional typeclass derivation (Show, Codec, Schema).
Use `opaque type` for type-safe wrappers: `opaque type UserId = Long`.
AI often generates plain classes or Java-style beans for domain data. Case classes are the idiomatic Scala choice and unlock pattern matching throughout the codebase.
Rule 3: Pattern matching — exhaustive and expressive
Use pattern matching as the primary dispatch mechanism:
response match
case Success(user) => process(user)
case Failure(e: NotFoundException) => notFound(e.message)
case Failure(e) => internalError(e)
Rules:
- Always handle all cases — enable `-Wnonexhaustive-match` compiler warning
- Use guard clauses in patterns: `case user if user.active => ...`
- Destructure deeply: `case User(id, _, Email(addr)) => ...`
- Prefer `match` expressions that return values over `match` with side effects
Pattern matching in Scala is more powerful than in most languages — AI underuses it. Exhaustive matching with compiler warnings is a major correctness tool.
Rule 4: Option, Either, Try — no null, no exceptions for flow
Error and absence handling:
- `Option[A]` for values that may be absent — never `null`
- `Either[Error, A]` for operations that can fail with a meaningful error
- `Try[A]` only at the boundary with exception-throwing Java libraries
- Never throw exceptions for business logic — use `Left(error)` or `Option.empty`
- Chain with `map`, `flatMap`, `fold`, `getOrElse`, `recover`
For complex chains: use for-comprehensions over nested flatMap calls.
null and exceptions as control flow are Java habits that leak into Scala. AI without guidance uses them. Option/Either with for-comprehensions is idiomatic.
Rule 5: For-comprehensions over nested flatMap
Use for-comprehensions for sequential monadic operations:
for
user <- findUser(id)
account <- findAccount(user.accountId)
_ <- validateBalance(account, amount)
yield Payment(user, account, amount)
Not:
findUser(id).flatMap(user =>
findAccount(user.accountId).flatMap(account =>
validateBalance(account, amount).map(_ => Payment(user, account, amount))
)
)
For-comprehensions work with any monad: Option, Either, Future, IO, ZIO.
For-comprehensions are syntactic sugar for flatMap/map chains. They're dramatically more readable for sequential operations. AI often generates nested lambda chains.
Rule 6: Immutability — val over var, immutable collections
Immutability rules:
- `val` by default — `var` only when mutation is genuinely required and documented
- Immutable collections: `List`, `Vector`, `Map`, `Set` from `scala.collection.immutable`
- Never import `scala.collection.mutable` without an explicit comment explaining why
- Use `copy` on case classes instead of mutating: `user.copy(name = "Alice")`
- Prefer `foldLeft`/`foldRight` over accumulator variables
If you need a mutable cell: use `Ref` (ZIO), `IORef` (Cats Effect), or `AtomicReference`.
Mutable state is the source of most concurrency bugs. Scala's type system enables functional immutability — AI needs to be told to use it.
Rule 7: Type classes — given/using, not implicit magic
Scala 3 type class pattern:
trait Show[A]:
def show(a: A): String
given Show[User] with
def show(u: User) = s"User(${u.id}, ${u.name})"
def display[A: Show](a: A): String = summon[Show[A]].show(a)
Rules:
- Use `given` for instances, `using` for parameters (not `implicit`)
- Derive type class instances where possible: `derives Show, Encoder, Decoder`
- Keep given instances in companion objects or dedicated `given` files
- Never use `implicit` — Scala 2 syntax is banned in Scala 3 style
The implicit → given/using migration is one of the biggest Scala 2 → Scala 3 changes. AI trained on Scala 2 generates implicit val and implicit def everywhere.
Rule 8: Collections — right tool for the operation
Collection selection:
- `List`: prepend-heavy workloads, pattern matching, recursive algorithms
- `Vector`: indexed access, append, large collections (O(log n) operations)
- `Map`/`Set`: lookups and membership tests
- `LazyList`: potentially infinite sequences, streaming data
- `Array`: only when interoperating with Java libraries that require it
Operations:
- `map`, `flatMap`, `filter`, `foldLeft` for transforms
- `groupBy`, `partition`, `span`, `takeWhile` for splitting
- `zipWithIndex` instead of index-based loops
- `traverse` (Cats) for `List[F[A]] => F[List[A]]` (effects in collections)
AI sometimes uses Array for everything (Java habit) or List where Vector is more appropriate. The right collection type matters for performance and idiomatic code.
Rule 9: Futures and concurrency
If using Scala Futures:
- Always provide an explicit `ExecutionContext` — never use `global` in production
- Use `Future.successful`, `Future.failed` for already-complete values
- Chain with `map`, `flatMap`, `recover`, `recoverWith`
- Use `Future.sequence` for parallel execution: `Future.sequence(List(f1, f2, f3))`
- Set timeouts at the boundary — Future itself has no timeout
If using ZIO or Cats Effect (preferred for new code):
- Use `ZIO[R, E, A]` / `IO[E, A]` instead of Future
- Effects are values — compose them without executing until the `main` method
- Use `ZIO.foreachPar` / `IO.parSequence` for parallel effects
- Structured concurrency: `ZIO.scoped` / `Resource` for resource management
Futures with implicit execution contexts are a common AI default. Specify which concurrency model your project uses — ZIO and Cats Effect have very different conventions.
Rule 10: Testing — ScalaTest or MUnit with property-based testing
Testing:
- Framework: ScalaTest (FlatSpec or FunSuite style) or MUnit
- Property-based testing: ScalaCheck for invariant verification
- Style: `"description" should "behavior"` (FlatSpec) or `test("description")` (MUnit/FunSuite)
- Async tests: use `Future`-aware or `IO`-aware test runners
- No `Thread.sleep` in tests — use async assertions or test schedulers
- Fixture setup: `beforeEach`/`afterEach` or `fixture.test`
For ZIO: use `zio-test` with `ZIOSpecDefault`.
For Cats Effect: use `munit-cats-effect`.
Scala testing frameworks are diverse. Without specifying, AI may mix styles or generate blocking tests for async code.
Rule 11: Error types — sealed hierarchies, not strings
Define errors as sealed traits:
sealed trait AppError
case class NotFound(id: String) extends AppError
case class ValidationError(field: String, message: String) extends AppError
case class DatabaseError(cause: Throwable) extends AppError
Use these as the Left side of Either[AppError, A].
Pattern match on them exhaustively — the compiler will warn if a case is missing.
Never use `String` or `Exception` as an error type in business logic.
String errors are untyped and non-exhaustive. Sealed trait hierarchies give compile-time completeness checking and make error handling explicit and safe.
Rule 12: sbt and dependency hygiene
sbt conventions:
- Pin all dependency versions explicitly in `build.sbt` — no `latest.release`
- Use `dependencyOverrides` for transitive version conflicts
- Organize: `libraryDependencies` grouped by: compile / test / provided
- Enable compiler flags: `-Wunused`, `-Wvalue-discard`, `-Xfatal-warnings` in CI
- Separate modules in a multi-project build for clean dependency boundaries
- Use `scalafmt` for formatting — config in `.scalafmt.conf`, enforced in CI
AI sometimes generates %% version ranges or omits important compiler flags. Strict compiler settings catch bugs before runtime.
Rule 13: Logging with structured context
Logging:
- Use SLF4J + Logback (or ZIO Logging / log4cats for effect systems)
- Structured logging preferred: JSON output in production
- Every log call includes context: `logger.info("Payment processed", Map("userId" -> userId, "amount" -> amount))`
- Log levels: trace/debug (dev), info (normal), warn (degraded), error (failure)
- No `println` or `System.out.println` in production code
- Correlation IDs: propagate via MDC (SLF4J) or ZIO FiberRef / Cats Effect IOLocal
Your CLAUDE.md starting point
# Scala Project — AI Coding Rules
## Version
Scala 3.x. No implicit keyword — use given/using.
## Data
Case classes for domain models. opaque type for type-safe wrappers.
Immutable by default. val over var. copy() over mutation.
## Error Handling
Option for absence. Either[AppError, A] for fallible operations. No null. No exceptions for business logic.
Sealed trait hierarchies for error types — exhaustive pattern matching enforced.
## Style
For-comprehensions over nested flatMap. Pattern matching as primary dispatch.
Immutable collections: List/Vector/Map/Set.
## Effects
[ZIO / Cats Effect / Future — specify]. Effects are values. No Thread.sleep. Structured concurrency.
## Testing
[ScalaTest FunSuite / MUnit / zio-test]. Property-based with ScalaCheck. Async-aware.
## Build
sbt. Pinned dependency versions. scalafmt enforced. -Wunused -Xfatal-warnings in CI.
The gap this closes
The same Scala code compiles whether it's idiomatic or not. The difference between Java-in-Scala and real Scala shows up in maintainability, performance characteristics, and how well the compiler catches bugs.
CLAUDE.md is what makes AI output land on the right side of that line — consistently, across sessions and team members.
The full rules pack across 14+ languages is at gumroad — $27.
Top comments (0)