Scala standards
Criteria verified as of August 2026. Re-verify on the web before committing to anything (§8).
1. Scope and triggers
Applies to all Scala code: .scala/.sc files, build.sbt, project/build.properties, project/plugins.sbt, project/*.scala, build.mill/build.sc, scala-cli //> using directives, .scalafmt.conf, .scalafix.conf, .jvmopts, and the pipelines that compile/test Scala. Covers backend services, libraries, CLIs, jobs and the structural choice of effect ecosystem. It fixes criteria: what to use, what is forbidden, what to verify.
Not applicable: see jvm-spring-standards (the JVM itself and Spring: JDK distribution and version, LTS, GC and its tuning, startup flags, JFR, containerising the JVM, jlink/GraalVM, and every Spring/Spring Boot criterion — if a Scala project uses Spring, jvm-spring-standards rules for Spring; here only the Scala code, its native builds — sbt/Mill/scala-cli — and its libraries), clojure-standards (the catalogue's other JVM language: same runtime, opposite paradigms — structural static typing versus dynamic homoiconic; the choice between them is one of team and ecosystem, not performance), data-engineering-standards and lakehouse-standards (Spark is theirs as a platform: pipeline, orchestration, partitioning, table format and cost; the Scala written inside the job — API, types, tests, build — belongs to this skill), streaming-cdc-standards (Flink/Kafka Streams as a streaming platform; here only the job's Scala code), api-design-standards (design of the HTTP/gRPC contract; here only its implementation with http4s/tapir/ZIO HTTP/Pekko HTTP), sql-standards (SQL itself: schema, indexes, plans — Doobie/Slick/quill generate SQL and that SQL is subject to their criteria), data-platform-standards (engine modelling and tuning), microservices-architecture-standards (service cuts, sagas, distributed resilience), appsec-standards (threat modelling and agnostic vulnerability classes; here only Scala's concrete sinks), vulnerability-management-standards (CVE triage and SLAs; here only the build gate), secrets-management-standards, observability-standards (the OTel pipeline; here only in-code instrumentation), cicd-standards (the pipeline that runs the gates), kubernetes-standards (OCI image and deployment), python-standards/go-standards/rust-standards (choosing a language over Scala), groovy-standards (the catalogue's third JVM language, with a different role: today it is mostly a configuration language for other tools — Gradle's Groovy DSL and the Jenkinsfile are theirs, including the build.gradle of a Scala project that uses Gradle instead of sbt).
2. Toolchain and default decisions
Verify the latest version on the web before pinning it in a real project (§8).
| Piece |
Choice |
Status as of 2026-08 |
Note |
| Language |
Scala 3 |
Next 3.8.4; LTS 3.3.8; 3.9.0 in RC (RC4) |
3.9 will be the new LTS line; 3.8 freezes features ahead of it |
| Baseline JDK |
3.8+/3.9 require JDK 17+; 3.3 LTS is still compatible with JDK 8 |
— |
The JDK version is set by jvm-spring-standards; here only the minimum the compiler imposes |
| Scala 2 |
2.13.18 (maintained line); 2.12 only because of an external dependency (Spark) |
— |
New code in Scala 2 requires written justification |
| Build |
sbt 2.0.x (GA 2026-06-14; 2.0.4 current) for new projects; sbt 1.12.x still maintained |
— |
sbt 2 requires JDK 17+ and plugins written in Scala 3 |
| Alternative build |
Mill 1.x (1.1.7) if the team prioritises build speed and readability |
— |
A team decision, not a purely technical one: fewer plugins than sbt |
| Scripts/prototypes |
scala-cli 1.16.x |
— |
The standard for scripts, bug reproductions and documentation examples |
| Formatting |
scalafmt 3.11.x (Apache-2.0) |
— |
.scalafmt.conf versioned with version and runner.dialect pinned |
| Refactor/lint |
scalafix 0.14.x (BSD-3-Clause) |
— |
Semantic rules: RemoveUnused, OrganizeImports, DisableSyntax |
| Test |
munit 1.3.x by default; ScalaCheck 1.19.x for properties |
ScalaTest 3.2.20 (no release since 2024-06) |
ScalaTest only if the repository already uses it; weaver now lives in typelevel/weaver-test (0.13.x), active |
| JSON |
jsoniter-scala 2.39.x (performance) or circe 0.14.x (ergonomics) |
— |
Pick one per repository; do not mix |
| Binary compatibility |
MiMa in every published library |
— |
A CI gate, not a report |
Effect ecosystem — a structural decision, taken once per organisation and documented in an ADR. The three stacks are mutually exclusive in practice: mixing them duplicates runtime, error types, thread pools and learning curve.
| Stack |
When |
Status as of 2026-08 |
| Typelevel (cats-effect 3.7, fs2 3.13, http4s 0.23.x, doobie 1.0.0-RCx) |
Recommended default: the de facto standard effect model, broad interoperability, Apache-2.0 licences |
cats-effect 3.7.0 (2025-07); stable http4s is still on the 0.23.x line — 1.0 has been in milestones for years (M47); doobie is still at 1.0.0-RC13 (a long-running RC) |
| ZIO (2.1.x, zio-http 3.11.x) |
Teams that want an integrated, opinionated single-vendor stack (effect, streams, HTTP, test, config) |
ZIO 2.1.26 (2026-05). No evidence of a published ZIO 3 |
| Akka / Apache Pekko |
Only with a real need for the actor model, cluster sharding or event sourcing (Akka Persistence) |
Akka: repository moved to akka/akka-core, 2.10.x, BSL. Pekko: ASF TLP, 1.6.0 stable and 2.0.0-Mx in development |
The Akka case is the most expensive licensing decision in the ecosystem. Verified:
- 2022-09-07: Lightbend announces the change. Verbatim: "The new license for Akka is the Business Source License (BSL) v1.1", "After 3 years, the BSL license indefinitely reverts to an Apache 2.0 license" and "The commercial license will be available at no charge for early-stage companies (less than US $25 million in annual revenue)". The change applies from Akka 2.7; published versions ≤2.6.x remain under Apache-2.0. BSL is not an open source licence.
- Apache Pekko is the community fork from Akka 2.6.x, today an ASF Top-Level Project (graduation announced by the ASF on 2024-05-16), Apache-2.0, with equivalent modules (actor, streams, http, persistence, connectors) and adopted by Flink and Play.
- Criterion: for new code, Pekko, not Akka. Akka only with a current commercial licence, reviewed by legal and with the revenue threshold checked against the current terms (§8) — the $25M threshold is the one from the 2022 announcement, not an eternal figure. Migrating Akka 2.6.x → Pekko is mechanical (package renaming) and is the default path for any base still on 2.6.x.
- Watch the transitive dependency: a library dragging in
com.typesafe.akka ≥2.7 puts BSL in your tree without anyone deciding it. Veto it in the licence policy (§5).
3. Structure and conventions
Scala 3 is the present, with two verified caveats that change the migration plan:
- Interoperability is no longer bidirectional. All of Scala 3 consumes 2.13 artifacts; the 2.13 TASTy reader (
-Ytasty-reader) will never be able to consume Scala 3.8+ artifacts — 3.7 is the last line readable from Scala 2. Cause: 3.8 publishes scala-library compiled with Scala 3.
- Operational consequence: if modules remain on 2.13 consuming Scala 3 libraries, either they are pinned to artifacts ≤3.7 or the migration gets finished. The TASTy reader is a migration aid with an expiry date, not a permanent compatibility layer.
Compatibility to know before publishing: Scala 3 minors are backwards TASTy- and binary-compatible ("code compiled with Scala 3.2.x can still be used in Scala 3.3.x, 3.4.x, 3.5.x or any other future version, indefinitely"); patches are compatible in both directions. Derived rule: libraries are published against LTS (3.3 today, 3.9 tomorrow); applications run on Next.
- Cross-building only when there are real consumers on the other version. Cross 2.13+3 in an application is pure cost.
- Standard layout
src/main/scala / src/test/scala; multi-module sbt/Mill when there are boundaries to protect (domain with no framework dependencies, adapters apart). Packages by domain, not by technical layer.
given/using with discipline: type class instances in the companion of the type or of the type class (predictable implicit search); given always with a name and an explicit type in a public API. Forbidden to use using to pass business configuration or request context "because it is convenient" — that is a parameter. Implicit conversions: only through an explicit Conversion imported at the point of use; never a global implicit conversion.
extension replaces implicit classes; group extensions by type and do not export methods that collide with the type's API.
- Opaque types (
opaque type) for identifiers and units (opaque type UserId = UUID) — zero runtime cost and they prevent accidentally swapping two Strings. A newtype with case class only when pattern matching is needed.
- ADTs with
enum (including hierarchies with parameters) and sealed trait only if the hierarchy needs more than enum expresses. Impossible states unrepresentable; a case class with scattered Options/booleans is the antipattern to avoid.
- Typed errors, not exceptions:
Either[DomainError, A] (or the effect's error channel: IO+EitherT/MonadError, ZIO[R, E, A]) for every expected domain failure. Exceptions are left for non-recoverable failures and for the boundary with Java. Forbidden: try/catch as control flow, and catching Throwable/NonFatal to swallow the error.
Future is discouraged in new code: eager evaluation (not referentially transparent), no cancellation, no bound resources. It is admitted only at the boundary with Java/Play/Slick libraries that impose it, converting immediately to the stack's effect (IO.fromFuture, ZIO.fromFuture).
- Resources always bound:
Resource/Scope (or Using in effect-free code). No manual close() hoping no exception fires first.
null does not exist in Scala except in Java interop: wrap in Option on the call line, not three layers up. Option.get, head on a possibly empty collection and .asInstanceOf are forbidden (§7).
Scala.js and Scala Native — bounded. Scala.js 1.22.x is the mature option for sharing the domain model and validations between backend and web frontend; the rest of the frontend criteria (bundling, framework, accessibility) is not this skill's business. Scala Native 0.5.x is viable for CLIs and fast startup (cats-effect 3.7 adds multithreading on Native), but it implies a reduced ecosystem and different debugging: adopt it with a written use case, not out of fashion.
4. Quality: formatting, flags, lint and testing
Scala 3 compiler flags that are worth it — set in the build's scalacOptions, not in the IDE:
scalacOptions ++= Seq(
"-encoding", "UTF-8",
"-deprecation", "-feature", "-unchecked",
"-Wunused:all", // available from 3.3.4+
"-Wvalue-discard", // silently discarded results
"-Wnonunit-statement",
"-Werror", // see note
"-Wconf:cat=deprecation:ws,any:e", // deprecations as warnings, everything else an error
"-source:3.8" // pin the source level explicitly
)
-Werror, not -Xfatal-warnings: verified — -Xfatal-warnings is a legacy alias deprecated since Scala 3.8. -Wall exists from 3.5.2+ (backported to 3.3.5+) as a broad switch.
-Werror always on in CI; locally it can be relaxed (-Wconf), but the CI build is the truth. One-off exceptions with a narrow @nowarn("msg=...") and a comment explaining why, never a global -Wconf:any:s.
- When running scalafix, disable fatal warnings for that task: otherwise the compiler breaks the build before scalafix can fix anything.
- sbt-tpolecat is acceptable so as not to maintain the list by hand, but review what it enables: it inherits flags that may lag behind 3.8's deprecations.
Formatting and refactoring. scalafmt with version pinned in .scalafmt.conf (different versions reformat differently → phantom diffs); scalafmtCheckAll in CI. scalafix with RemoveUnused, OrganizeImports and DisableSyntax configured to veto at build time what §7 forbids (null, throw, var, asInstanceOf, Option.get) in domain modules.
Testing.
- munit by default (fast, no bespoke DSL to learn);
munit-cats-effect/zio-test depending on the stack. ScalaTest only through repository inertia.
- ScalaCheck / property-based testing is mandatory in all logic with invariants (parsers, round-trip serialisation, domain arithmetic, business rules with combinatorial cases). One well-chosen property test replaces twenty examples.
- Coverage of edges and errors: every variant of the error ADT, empty collections, numeric limits, timeouts and cancellation of the effect (a test that only exercises
IO's happy path proves nothing about its semantics).
- Virtual time (
TestControl in cats-effect, TestClock in ZIO) instead of real sleeps; Thread.sleep and Await.result are forbidden in tests.
- Integration against real dependencies with testcontainers-scala (same image and version as production); H2 as a substitute for the real database is forbidden.
- Every bugfix leaves a regression test. A flaky test is fixed or deleted.
- Coverage (
scoverage) as a signal, never as a goal.
CI gates, in order of increasing cost (they break the build):
scalafmtCheckAll → scalafixAll --check → compile with -Werror → test → mimaReportBinaryIssues (libraries) → dependency SCA
Main always green; nothing is merged on red CI.
Compilation times are an engineering problem, not a complaint. A build cycle measured in minutes destroys feedback and pushes the team into skipping tests.
- An explicit budget (e.g. incremental compilation of one module <20 s) and measurement with
-Vprofile / -Ystatistics when it degrades.
- Usual, actionable causes: macros and aggressive inlining, automatic codec derivation at every point of use (derive once in the companion, not
deriveEncoder inline per call), deep implicit search, very generic type hierarchies, monolithic modules.
- Split into modules along real dependencies, use
Test / parallelExecution judiciously, bloop/a build server locally, CI caching (sbt 2 ships a Bazel-compatible cache; Mill caches per task).
- Before "optimising Scala", measure: most slow builds are one module and three files.
5. Stack security
- Deserialisation: the JVM's real risk. Java native serialisation of external data is forbidden, as is any open polymorphic deserialisation (Jackson
enableDefaultTyping, @JsonTypeInfo without an allowlist). In Scala this is solved with codecs derived from closed types: jsoniter-scala (compile-time macros, no reflection, the fastest) or circe (explicit deriveDecoder in the companion). Every ADT deserialised from external input carries an explicit discriminator and a closed set of subtypes.
- Limits when parsing untrusted input: maximum body size in the HTTP server, JSON depth/length (jsoniter exposes
ReaderConfig with maxBufSize/depth), read timeouts. A decoder without limits is a DoS.
- Validate at the edge with types: parse into a domain type (
Either[Error, Email]), do not validate and carry on with String. Parse, don't validate.
- Parameterised SQL only:
doobie/Slick/quill with safe interpolation (doobie's sql"..." is parameterised); building SQL with s"..." or + over input is forbidden. The criteria for the resulting SQL belong to sql-standards.
- Dependencies in an ecosystem with a history of binary breakage — a hard rule:
evictionErrorLevel set to error and zero unmanaged evictions: two different minor versions of a Scala library on the classpath is a NoSuchMethodError in production waiting for its moment.
- Align the whole stack to a single line (a complete cats-effect 3.x; do not mix ZIO 1 and 2 modules).
sbt-dependency-graph/dependencyBrowseTree to audit the tree before adding anything; every new dependency is a trust decision that gets justified.
- A library with no release in >18 months or with an eternal RC: accepted only with a recorded decision (the doobie 1.0.0-RCx and http4s 0.23.x cases: they are stable de facto and widely deployed, but the numbering is no guarantee — the guarantee is the project's binary compatibility policy).
- Licence policy in the gate: an allowlist (Apache-2.0, MIT, BSD, EPL-2.0…) and an explicit denylist of BSL/SSPL/Elastic — so that transitive Akka ≥2.7 breaks the build instead of turning up in an audit.
- SCA in CI: dependency scanning (the specific scanner and the triage SLA are set by
vulnerability-management-standards) that breaks the build on an exploitable critical/high CVE. Scheduled as well as on the PR, because CVEs appear without the code changing.
- Build supply chain: sbt plugins and Mill tasks execute arbitrary code with the runner's permissions. Plugins pinned by version, from known sources, reviewed on update;
project/build.properties versioned; no unencrypted HTTP resolvers and no snapshots in release builds.
- Secrets: never in
build.sbt, a versioned application.conf, logs or exception messages. Typed config (pureconfig/ciris/zio-config) with secrets from the environment or a manager; wrap them in a type that redacts in toString. A Scala-specific hazard: case class generates a toString that prints every field — a logged case class Config(dbPassword: String) leaks the password; override toString or use an opaque Secret.
- Crypto and TLS: those of the JDK/standard libraries (see
cryptography-pki-standards for algorithm choice). Security randomness with SecureRandom, never scala.util.Random.
- Logs without PII; domain exceptions without sensitive data in the message (they end up in error responses and in the SIEM).
6. Performance and operability
- The JVM (GC, heap, startup flags, JFR, container) is
jvm-spring-standards territory. Here, the Scala-specific parts:
- Never block the effect's compute pool: JDBC, files, blocking Java calls and heavy CPU go to a separate context (
IO.blocking/Sync[F].blocking, ZIO.attemptBlocking, a dedicated dispatcher in Pekko). Blocking the work-stealing pool turns a service into a silent hang under load.
- Explicit backpressure: fs2/ZIO Streams/Pekko Streams with concurrency limits (
parEvalMap(n), mapZIOPar), bounded queues, and no parTraverse over a list of unbounded size (unlimited fan-out against a dependency = incident).
- Timeouts at every edge (HTTP client, JDBC, connection pool) and propagated cancellation; effects are cancellable — make sure the operation is cancel-safe and releases resources.
- Collections:
List for sequential access, Vector/ArraySeq for indexed access, LazyList with care (it memoises). Forbidden: ++ in a loop over immutable structures (quadratic) and using Seq in a public API where performance matters (it may be a linked list). view to chain transformations over large collections.
- Benchmarks with JMH (
sbt-jmh) before claiming anything is faster; boxing and implicit collection conversions are the usual suspects.
- Observability:
otel4s/zio-telemetry/OTel instrumentation depending on the stack; structured logs (log4cats, ZIO Logging) with traceId. println is forbidden in service code. The pipeline and the SLOs belong to observability-standards.
- Graceful shutdown is mandatory:
IOApp/ZIOAppDefault handle SIGTERM and finalizers; verify that the server drains connections and that Resource/Scope close pools before exiting, aligned with the orchestrator's grace period.
- Separate health endpoints: trivial liveness, readiness that checks dependencies.
7. Sustainability: upgrades and prohibitions
Cadence. Applications on Scala Next, moving up a minor in the quarter following its release; published libraries against LTS, with the LTS jump planned within the year following the new line (the previous LTS has patches guaranteed "until at least a year after the release of the next LTS line"). Dependencies with Scala Steward (the ecosystem standard) grouped weekly; majors by hand with the changelog. Migration 2.13 → 3: plan it before the TASTy reader's asymmetry makes it urgent (§3).
Deprecation. @deprecated with since and a message naming the replacement; a window of at least one minor before removal. In libraries, SemVer + MiMa in CI: breaking binary compatibility without a major bump is a consumer incident, not a detail.
Conscious debt. Every shortcut with // TODO(user): reason — link to issue.
FORBIDDEN (exception only with written justification and approval):
- ❌
null in Scala code (except Java interop, wrapped on the spot); Option.get, .head/.last on a possibly empty collection, .asInstanceOf, isInstanceOf as a substitute for pattern matching.
- ❌
throw as control flow for domain errors; catch { case _ => } or case NonFatal(_) => () that swallows the error without a decision; catching Throwable.
- ❌
Await.result/Await.ready in production code; unsafeRunSync()/Unsafe.unsafe outside main or a test.
- ❌
Future in new code outside the boundary with a library that imposes it.
- ❌ Blocking the effect's compute pool (JDBC/synchronous IO/heavy CPU without
blocking).
- ❌ Mixing effect ecosystems (cats-effect + ZIO + Pekko) in the same service without an explicit, approved interop layer.
- ❌ Akka ≥2.7 (BSL) without a current commercial licence verified by legal, including one arriving as a transitive dependency; for new code, Pekko.
- ❌ Global implicit conversions;
given without an explicit type in a public API; using to smuggle in business configuration.
- ❌
var and shared mutable state in the domain; mutable.Map as a global cache with no concurrency control and no TTL.
- ❌ Inline codec derivation at every point of use (compilation cost) and
deriveEncoder over open types with untrusted input.
- ❌ Unmanaged dependency evictions; two minor versions of the same library on the classpath;
+ (dynamic versions) or snapshots in release builds.
- ❌ CI without
-Werror; a global -Wconf:any:s; warning suppression with no written reason; merging on red CI or with flaky tests.
- ❌
println/printStackTrace instead of structured logging; a config case class holding secrets with the default toString.
- ❌ Concatenating input into SQL; Java native serialisation or open polymorphic deserialisation of external data.
- ❌ Starting a new project in Scala 2 without an ADR; adding cross-building without real consumers.
- ❌ Home-grown macros/
inline for what an existing derivation or explicit code solves (compilation cost + opacity).
8. Mandatory web verification
Before pinning versions or asserting the state of the ecosystem, check online (not from memory):
- Scala: current Next and LTS versions and the JDK baseline — scala-lang.org/download/all.html and the release notes; has 3.9.0 final shipped as the new LTS? (as of Aug 2026 it was at RC4). Compatibility policy at scala-lang.org/development/.
- 2.13↔3 interop: scala-lang.org, "State of the TASTy reader" — confirm the cut at 3.8 and up to which version artifacts remain consumable from 2.13.
- sbt / Mill / scala-cli:
github.com/{sbt/sbt,com-lihaoyi/mill,VirtusLab/scala-cli}/releases.atom; the state of the sbt 2 plugin ecosystem (sbt2-compat) before forcing sbt 2 on a repository with in-house plugins.
- Akka's licence: akka.io/bsl-license-faq and akka.io/pricing — the revenue threshold and the terms may have changed since 2022; check with legal. Status and releases of Apache Pekko at pekko.apache.org (not just GitHub).
- Effect stacks: releases of cats-effect/fs2/http4s/doobie and of ZIO; check whether http4s 1.0 or doobie 1.0 finals have shipped and whether ZIO published a new line.
- Tools: scalafmt, scalafix, munit, ScalaCheck, MiMa, testcontainers-scala — latest version and licence (licence changes and projects in maintenance mode are a real risk to the catalogue).
- CVEs in the tree (jackson, netty, JDBC drivers, logging libraries) in the projects' advisories and NVD before freezing versions.
- Declared gap, unverified as of Aug 2026: the formal EOL date of the Scala 2.13 line and of 3.3 LTS (no published date located, only the relative guarantee "≥1 year after the next LTS"); ScalaTest's maintenance status (no releases since 2024-06, no official end-of-support statement found); Akka's exact current commercial terms.
If the web contradicts this document, the web wins — flag the discrepancy.
1---2name: scala-standards3description: Scala engineering standards (staff-level). Trigger on .scala/.sc files, build.sbt, project/build.properties, project/plugins.sbt, build.mill/build.sc, scala-cli "//> using" directives, .scalafmt.conf, .scalafix.conf, MiMa/binary-compatibility checks, and on Scala libraries such as cats-effect, fs2, http4s, ZIO, Akka, Apache Pekko, circe, jsoniter-scala, doobie, Slick, munit, ScalaTest, ScalaCheck, weaver, Scala.js or Scala Native. Apply when writing, reviewing, migrating (2.13 to 3) or configuring CI for Scala code, and when choosing between the Typelevel, ZIO and Akka/Pekko ecosystems.4---56# Scala standards78Criteria verified as of **August 2026**. Re-verify on the web before committing to anything (§8).910## 1. Scope and triggers1112Applies to all Scala code: `.scala`/`.sc` files, `build.sbt`, `project/build.properties`, `project/plugins.sbt`, `project/*.scala`, `build.mill`/`build.sc`, scala-cli `//> using` directives, `.scalafmt.conf`, `.scalafix.conf`, `.jvmopts`, and the pipelines that compile/test Scala. Covers backend services, libraries, CLIs, jobs and the structural choice of effect ecosystem. It fixes **criteria**: what to use, what is forbidden, what to verify.1314**Not applicable**: see `jvm-spring-standards` (**the JVM itself and Spring**: JDK distribution and version, LTS, GC and its tuning, startup flags, JFR, containerising the JVM, jlink/GraalVM, and every Spring/Spring Boot criterion — if a Scala project uses Spring, **`jvm-spring-standards` rules for Spring**; here only the Scala code, its native builds — sbt/Mill/scala-cli — and its libraries), `clojure-standards` (the catalogue's other JVM language: same runtime, opposite paradigms — structural static typing versus dynamic homoiconic; the choice between them is one of **team and ecosystem**, not performance), `data-engineering-standards` and `lakehouse-standards` (**Spark is theirs as a platform**: pipeline, orchestration, partitioning, table format and cost; **the Scala written inside the job — API, types, tests, build — belongs to this skill**), `streaming-cdc-standards` (Flink/Kafka Streams as a streaming platform; here only the job's Scala code), `api-design-standards` (design of the HTTP/gRPC contract; here only its implementation with http4s/tapir/ZIO HTTP/Pekko HTTP), `sql-standards` (**SQL itself**: schema, indexes, plans — Doobie/Slick/quill generate SQL and that SQL is subject to their criteria), `data-platform-standards` (engine modelling and tuning), `microservices-architecture-standards` (service cuts, sagas, distributed resilience), `appsec-standards` (threat modelling and agnostic vulnerability classes; here only Scala's concrete sinks), `vulnerability-management-standards` (CVE triage and SLAs; here only the build gate), `secrets-management-standards`, `observability-standards` (the OTel pipeline; here only in-code instrumentation), `cicd-standards` (the pipeline that runs the gates), `kubernetes-standards` (OCI image and deployment), `python-standards`/`go-standards`/`rust-standards` (choosing a language over Scala), `groovy-standards` (**the catalogue's third JVM language**, with a different role: today it is mostly a configuration language for other tools — **Gradle's Groovy DSL and the `Jenkinsfile` are theirs**, including the `build.gradle` of a Scala project that uses Gradle instead of sbt).1516## 2. Toolchain and default decisions1718> Verify the latest version on the web before pinning it in a real project (§8).1920| Piece | Choice | Status as of 2026-08 | Note |21|---|---|---|---|22| Language | **Scala 3** | Next **3.8.4**; LTS **3.3.8**; **3.9.0 in RC** (RC4) | 3.9 will be the new LTS line; 3.8 freezes features ahead of it |23| Baseline JDK | 3.8+/3.9 require **JDK 17+**; 3.3 LTS is still compatible with JDK 8 | — | The **JDK version is set by `jvm-spring-standards`**; here only the minimum the compiler imposes |24| Scala 2 | **2.13.18** (maintained line); 2.12 only because of an external dependency (Spark) | — | New code in Scala 2 **requires written justification** |25| Build | **sbt 2.0.x** (GA 2026-06-14; 2.0.4 current) for new projects; **sbt 1.12.x** still maintained | — | sbt 2 requires JDK 17+ and plugins written in Scala 3 |26| Alternative build | **Mill 1.x** (1.1.7) if the team prioritises build speed and readability | — | A team decision, not a purely technical one: fewer plugins than sbt |27| Scripts/prototypes | **scala-cli 1.16.x** | — | The standard for scripts, bug reproductions and documentation examples |28| Formatting | **scalafmt 3.11.x** (Apache-2.0) | — | `.scalafmt.conf` versioned with `version` and `runner.dialect` pinned |29| Refactor/lint | **scalafix 0.14.x** (BSD-3-Clause) | — | Semantic rules: `RemoveUnused`, `OrganizeImports`, `DisableSyntax` |30| Test | **munit 1.3.x** by default; **ScalaCheck 1.19.x** for properties | ScalaTest 3.2.20 (no release since 2024-06) | ScalaTest only if the repository already uses it; weaver now lives in **typelevel/weaver-test** (0.13.x), active |31| JSON | **jsoniter-scala 2.39.x** (performance) or **circe 0.14.x** (ergonomics) | — | Pick one per repository; do not mix |32| Binary compatibility | **MiMa** in every published library | — | A CI gate, not a report |3334**Effect ecosystem — a structural decision, taken once per organisation and documented in an ADR.** The three stacks are mutually exclusive in practice: mixing them duplicates runtime, error types, thread pools and learning curve.3536| Stack | When | Status as of 2026-08 |37|---|---|---|38| **Typelevel** (cats-effect 3.7, fs2 3.13, http4s 0.23.x, doobie 1.0.0-RCx) | Recommended default: the de facto standard effect model, broad interoperability, Apache-2.0 licences | cats-effect 3.7.0 (2025-07); stable http4s is still on the **0.23.x** line — 1.0 has been in milestones for years (M47); doobie is still at **1.0.0-RC13** (a long-running RC) |39| **ZIO** (2.1.x, zio-http 3.11.x) | Teams that want an integrated, opinionated single-vendor stack (effect, streams, HTTP, test, config) | ZIO 2.1.26 (2026-05). No evidence of a published ZIO 3 |40| **Akka / Apache Pekko** | Only with a real need for the **actor model**, cluster sharding or event sourcing (Akka Persistence) | Akka: repository moved to `akka/akka-core`, 2.10.x, **BSL**. Pekko: ASF TLP, 1.6.0 stable and 2.0.0-Mx in development |4142**The Akka case is the most expensive licensing decision in the ecosystem.** Verified:43- **2022-09-07**: Lightbend announces the change. Verbatim: *"The new license for Akka is the Business Source License (BSL) v1.1"*, *"After 3 years, the BSL license indefinitely reverts to an Apache 2.0 license"* and *"The commercial license will be available at no charge for early-stage companies (less than US $25 million in annual revenue)"*. The change applies from **Akka 2.7**; published versions ≤2.6.x remain under Apache-2.0. **BSL is not an open source licence.**44- **Apache Pekko** is the community fork from Akka 2.6.x, today an **ASF Top-Level Project** (graduation announced by the ASF on **2024-05-16**), Apache-2.0, with equivalent modules (actor, streams, http, persistence, connectors) and adopted by Flink and Play.45- **Criterion**: for new code, **Pekko**, not Akka. Akka only with a current commercial licence, reviewed by legal and with the revenue threshold checked against the **current** terms (§8) — the $25M threshold is the one from the 2022 announcement, not an eternal figure. Migrating Akka 2.6.x → Pekko is mechanical (package renaming) and is the default path for any base still on 2.6.x.46- Watch the transitive dependency: a library dragging in `com.typesafe.akka` ≥2.7 puts BSL in your tree without anyone deciding it. Veto it in the licence policy (§5).4748## 3. Structure and conventions4950**Scala 3 is the present, with two verified caveats that change the migration plan**:51- **Interoperability is no longer bidirectional.** All of Scala 3 consumes 2.13 artifacts; the 2.13 TASTy reader (`-Ytasty-reader`) **will never be able to consume Scala 3.8+ artifacts** — 3.7 is the last line readable from Scala 2. Cause: 3.8 publishes `scala-library` compiled with Scala 3.52- Operational consequence: if modules remain on 2.13 consuming Scala 3 libraries, either they are pinned to artifacts ≤3.7 or the migration gets finished. The TASTy reader is a **migration aid with an expiry date**, not a permanent compatibility layer.5354**Compatibility to know before publishing**: Scala 3 minors are *backwards* TASTy- and binary-compatible (*"code compiled with Scala 3.2.x can still be used in Scala 3.3.x, 3.4.x, 3.5.x or any other future version, indefinitely"*); patches are compatible in both directions. Derived rule: **libraries are published against LTS** (3.3 today, 3.9 tomorrow); applications run on Next.5556- **Cross-building only when there are real consumers** on the other version. Cross 2.13+3 in an application is pure cost.57- Standard layout `src/main/scala` / `src/test/scala`; multi-module sbt/Mill when there are boundaries to protect (domain with no framework dependencies, adapters apart). Packages by domain, not by technical layer.58- **`given`/`using` with discipline**: type class instances in the companion of the type or of the type class (predictable implicit search); `given` **always with a name and an explicit type** in a public API. Forbidden to use `using` to pass business configuration or request context "because it is convenient" — that is a parameter. Implicit conversions: only through an explicit `Conversion` imported at the point of use; never a global implicit conversion.59- **`extension`** replaces implicit classes; group extensions by type and do not export methods that collide with the type's API.60- **Opaque types (`opaque type`) for identifiers and units** (`opaque type UserId = UUID`) — zero runtime cost and they prevent accidentally swapping two `String`s. A newtype with `case class` only when pattern matching is needed.61- **ADTs with `enum`** (including hierarchies with parameters) and `sealed trait` only if the hierarchy needs more than `enum` expresses. Impossible states unrepresentable; a `case class` with scattered `Option`s/booleans is the antipattern to avoid.62- **Typed errors, not exceptions**: `Either[DomainError, A]` (or the effect's error channel: `IO`+`EitherT`/`MonadError`, `ZIO[R, E, A]`) for every expected domain failure. Exceptions are left for **non-recoverable** failures and for the boundary with Java. Forbidden: `try/catch` as control flow, and catching `Throwable`/`NonFatal` to swallow the error.63- **`Future` is discouraged in new code**: eager evaluation (not referentially transparent), no cancellation, no bound resources. It is admitted only at the boundary with Java/Play/Slick libraries that impose it, converting immediately to the stack's effect (`IO.fromFuture`, `ZIO.fromFuture`).64- **Resources always bound**: `Resource`/`Scope` (or `Using` in effect-free code). No manual `close()` hoping no exception fires first.65- `null` does not exist in Scala except in Java interop: wrap in `Option` **on the call line**, not three layers up. `Option.get`, `head` on a possibly empty collection and `.asInstanceOf` are forbidden (§7).6667**Scala.js and Scala Native — bounded.** Scala.js 1.22.x is the mature option for sharing the domain model and validations between backend and web frontend; the rest of the frontend criteria (bundling, framework, accessibility) **is not this skill's business**. Scala Native 0.5.x is viable for CLIs and fast startup (cats-effect 3.7 adds multithreading on Native), but it implies a reduced ecosystem and different debugging: adopt it with a written use case, not out of fashion.6869## 4. Quality: formatting, flags, lint and testing7071**Scala 3 compiler flags that are worth it** — set in the build's `scalacOptions`, not in the IDE:7273```scala74scalacOptions ++= Seq(75 "-encoding", "UTF-8",76 "-deprecation", "-feature", "-unchecked",77 "-Wunused:all", // available from 3.3.4+78 "-Wvalue-discard", // silently discarded results79 "-Wnonunit-statement",80 "-Werror", // see note81 "-Wconf:cat=deprecation:ws,any:e", // deprecations as warnings, everything else an error82 "-source:3.8" // pin the source level explicitly83)84```85- **`-Werror`, not `-Xfatal-warnings`**: verified — `-Xfatal-warnings` is a legacy alias **deprecated since Scala 3.8**. `-Wall` exists from 3.5.2+ (backported to 3.3.5+) as a broad switch.86- `-Werror` **always on in CI**; locally it can be relaxed (`-Wconf`), but the CI build is the truth. One-off exceptions with a narrow `@nowarn("msg=...")` and a comment explaining why, never a global `-Wconf:any:s`.87- When running scalafix, disable fatal warnings for that task: otherwise the compiler breaks the build before scalafix can fix anything.88- **sbt-tpolecat** is acceptable so as not to maintain the list by hand, but review what it enables: it inherits flags that may lag behind 3.8's deprecations.8990**Formatting and refactoring.** `scalafmt` with `version` pinned in `.scalafmt.conf` (different versions reformat differently → phantom diffs); `scalafmtCheckAll` in CI. `scalafix` with `RemoveUnused`, `OrganizeImports` and **`DisableSyntax`** configured to veto at build time what §7 forbids (`null`, `throw`, `var`, `asInstanceOf`, `Option.get`) in domain modules.9192**Testing.**93- **munit** by default (fast, no bespoke DSL to learn); `munit-cats-effect`/`zio-test` depending on the stack. ScalaTest only through repository inertia.94- **ScalaCheck / property-based testing is mandatory** in all logic with invariants (parsers, round-trip serialisation, domain arithmetic, business rules with combinatorial cases). One well-chosen property test replaces twenty examples.95- Coverage of **edges and errors**: every variant of the error ADT, empty collections, numeric limits, timeouts and **cancellation** of the effect (a test that only exercises `IO`'s happy path proves nothing about its semantics).96- Virtual time (`TestControl` in cats-effect, `TestClock` in ZIO) instead of real sleeps; **`Thread.sleep` and `Await.result` are forbidden in tests**.97- Integration against real dependencies with **testcontainers-scala** (same image and version as production); H2 as a substitute for the real database is forbidden.98- Every bugfix leaves a regression test. A flaky test is fixed or deleted.99- Coverage (`scoverage`) as a signal, never as a goal.100101**CI gates, in order of increasing cost (they break the build):**102```103scalafmtCheckAll → scalafixAll --check → compile with -Werror → test → mimaReportBinaryIssues (libraries) → dependency SCA104```105Main always green; nothing is merged on red CI.106107**Compilation times are an engineering problem, not a complaint.** A build cycle measured in minutes destroys feedback and pushes the team into skipping tests.108- An explicit budget (e.g. incremental compilation of one module <20 s) and measurement with `-Vprofile` / `-Ystatistics` when it degrades.109- Usual, actionable causes: macros and aggressive inlining, automatic codec derivation at every point of use (**derive once in the companion, not `deriveEncoder` inline per call**), deep implicit search, very generic type hierarchies, monolithic modules.110- Split into modules along real dependencies, use `Test / parallelExecution` judiciously, `bloop`/a build server locally, CI caching (sbt 2 ships a Bazel-compatible cache; Mill caches per task).111- Before "optimising Scala", measure: most slow builds are one module and three files.112113## 5. Stack security114115- **Deserialisation**: the JVM's real risk. Java native serialisation of external data is forbidden, as is any open polymorphic deserialisation (Jackson `enableDefaultTyping`, `@JsonTypeInfo` without an allowlist). In Scala this is solved with codecs **derived from closed types**: `jsoniter-scala` (compile-time macros, no reflection, the fastest) or `circe` (explicit `deriveDecoder` in the companion). Every ADT deserialised from external input carries an explicit discriminator and a closed set of subtypes.116- Limits when parsing untrusted input: maximum body size in the HTTP server, JSON depth/length (jsoniter exposes `ReaderConfig` with `maxBufSize`/depth), read timeouts. A decoder without limits is a DoS.117- **Validate at the edge with types**: parse into a domain type (`Either[Error, Email]`), do not validate and carry on with `String`. *Parse, don't validate.*118- **Parameterised SQL only**: `doobie`/`Slick`/`quill` with safe interpolation (doobie's `sql"..."` is parameterised); building SQL with `s"..."` or `+` over input is forbidden. The criteria for the resulting SQL belong to `sql-standards`.119- **Dependencies in an ecosystem with a history of binary breakage** — a hard rule:120 - `evictionErrorLevel` set to error and **zero unmanaged evictions**: two different minor versions of a Scala library on the classpath is a `NoSuchMethodError` in production waiting for its moment.121 - Align the whole stack to a single line (a complete cats-effect 3.x; do not mix ZIO 1 and 2 modules).122 - `sbt-dependency-graph`/`dependencyBrowseTree` to audit the tree before adding anything; every new dependency is a trust decision that gets justified.123 - A library with no release in >18 months or with an eternal RC: accepted only with a recorded decision (the doobie 1.0.0-RCx and http4s 0.23.x cases: they are stable *de facto* and widely deployed, but the numbering is no guarantee — the guarantee is the project's binary compatibility policy).124 - **Licence policy in the gate**: an allowlist (Apache-2.0, MIT, BSD, EPL-2.0…) and an **explicit denylist of BSL/SSPL/Elastic** — so that transitive Akka ≥2.7 breaks the build instead of turning up in an audit.125- **SCA in CI**: dependency scanning (the specific scanner and the triage SLA are set by `vulnerability-management-standards`) that breaks the build on an exploitable critical/high CVE. Scheduled as well as on the PR, because CVEs appear without the code changing.126- **Build supply chain**: sbt plugins and Mill tasks **execute arbitrary code with the runner's permissions**. Plugins pinned by version, from known sources, reviewed on update; `project/build.properties` versioned; no unencrypted HTTP resolvers and no snapshots in release builds.127- **Secrets**: never in `build.sbt`, a versioned `application.conf`, logs or exception messages. Typed config (`pureconfig`/`ciris`/`zio-config`) with secrets from the environment or a manager; wrap them in a type that redacts in `toString`. A Scala-specific hazard: **`case class` generates a `toString` that prints every field** — a logged `case class Config(dbPassword: String)` leaks the password; override `toString` or use an opaque `Secret`.128- Crypto and TLS: those of the JDK/standard libraries (see `cryptography-pki-standards` for algorithm choice). Security randomness with `SecureRandom`, never `scala.util.Random`.129- Logs without PII; domain exceptions without sensitive data in the message (they end up in error responses and in the SIEM).130131## 6. Performance and operability132133- **The JVM (GC, heap, startup flags, JFR, container) is `jvm-spring-standards` territory.** Here, the Scala-specific parts:134- **Never block the effect's compute pool**: JDBC, files, blocking Java calls and heavy CPU go to a separate context (`IO.blocking`/`Sync[F].blocking`, `ZIO.attemptBlocking`, a dedicated dispatcher in Pekko). Blocking the work-stealing pool turns a service into a silent hang under load.135- **Explicit backpressure**: fs2/ZIO Streams/Pekko Streams with concurrency limits (`parEvalMap(n)`, `mapZIOPar`), **bounded** queues, and no `parTraverse` over a list of unbounded size (unlimited fan-out against a dependency = incident).136- **Timeouts at every edge** (HTTP client, JDBC, connection pool) and propagated cancellation; effects are cancellable — make sure the operation is *cancel-safe* and releases resources.137- **Collections**: `List` for sequential access, `Vector`/`ArraySeq` for indexed access, `LazyList` with care (it memoises). Forbidden: `++` in a loop over immutable structures (quadratic) and using `Seq` in a public API where performance matters (it may be a linked list). `view` to chain transformations over large collections.138- Benchmarks with **JMH** (`sbt-jmh`) before claiming anything is faster; boxing and implicit collection conversions are the usual suspects.139- **Observability**: `otel4s`/`zio-telemetry`/OTel instrumentation depending on the stack; structured logs (`log4cats`, `ZIO Logging`) with traceId. `println` is forbidden in service code. The pipeline and the SLOs belong to `observability-standards`.140- **Graceful shutdown is mandatory**: `IOApp`/`ZIOAppDefault` handle SIGTERM and finalizers; verify that the server drains connections and that `Resource`/`Scope` close pools before exiting, aligned with the orchestrator's grace period.141- Separate health endpoints: trivial liveness, readiness that checks dependencies.142143## 7. Sustainability: upgrades and prohibitions144145**Cadence.** Applications on **Scala Next**, moving up a minor in the quarter following its release; published libraries against **LTS**, with the LTS jump planned within the year following the new line (the previous LTS has patches guaranteed *"until at least a year after the release of the next LTS line"*). Dependencies with Scala Steward (the ecosystem standard) grouped weekly; majors by hand with the changelog. Migration 2.13 → 3: plan it before the TASTy reader's asymmetry makes it urgent (§3).146147**Deprecation.** `@deprecated` with `since` and a `message` naming the replacement; a window of at least one minor before removal. In libraries, SemVer + **MiMa** in CI: breaking binary compatibility without a major bump is a consumer incident, not a detail.148149**Conscious debt.** Every shortcut with `// TODO(user): reason — link to issue`.150151**FORBIDDEN** (exception only with written justification and approval):152- ❌ `null` in Scala code (except Java interop, wrapped on the spot); `Option.get`, `.head`/`.last` on a possibly empty collection, `.asInstanceOf`, `isInstanceOf` as a substitute for pattern matching.153- ❌ `throw` as control flow for domain errors; `catch { case _ => }` or `case NonFatal(_) => ()` that swallows the error without a decision; catching `Throwable`.154- ❌ `Await.result`/`Await.ready` in production code; `unsafeRunSync()`/`Unsafe.unsafe` outside `main` or a test.155- ❌ `Future` in new code outside the boundary with a library that imposes it.156- ❌ Blocking the effect's compute pool (JDBC/synchronous IO/heavy CPU without `blocking`).157- ❌ Mixing effect ecosystems (cats-effect + ZIO + Pekko) in the same service without an explicit, approved interop layer.158- ❌ **Akka ≥2.7 (BSL) without a current commercial licence verified by legal**, including one arriving as a transitive dependency; for new code, Pekko.159- ❌ Global implicit conversions; `given` without an explicit type in a public API; `using` to smuggle in business configuration.160- ❌ `var` and shared mutable state in the domain; `mutable.Map` as a global cache with no concurrency control and no TTL.161- ❌ Inline codec derivation at every point of use (compilation cost) and `deriveEncoder` over open types with untrusted input.162- ❌ Unmanaged dependency evictions; two minor versions of the same library on the classpath; `+` (dynamic versions) or snapshots in release builds.163- ❌ CI without `-Werror`; a global `-Wconf:any:s`; warning suppression with no written reason; merging on red CI or with flaky tests.164- ❌ `println`/`printStackTrace` instead of structured logging; a config `case class` holding secrets with the default `toString`.165- ❌ Concatenating input into SQL; Java native serialisation or open polymorphic deserialisation of external data.166- ❌ Starting a new project in Scala 2 without an ADR; adding cross-building without real consumers.167- ❌ Home-grown macros/`inline` for what an existing derivation or explicit code solves (compilation cost + opacity).168169## 8. Mandatory web verification170171Before pinning versions or asserting the state of the ecosystem, check online (not from memory):1721. **Scala**: current Next and LTS versions and the JDK baseline — scala-lang.org/download/all.html and the release notes; has **3.9.0 final** shipped as the new LTS? (as of Aug 2026 it was at RC4). Compatibility policy at scala-lang.org/development/.1732. **2.13↔3 interop**: scala-lang.org, "State of the TASTy reader" — confirm the cut at 3.8 and up to which version artifacts remain consumable from 2.13.1743. **sbt / Mill / scala-cli**: `github.com/{sbt/sbt,com-lihaoyi/mill,VirtusLab/scala-cli}/releases.atom`; the state of the sbt 2 plugin ecosystem (sbt2-compat) before forcing sbt 2 on a repository with in-house plugins.1754. **Akka's licence**: akka.io/bsl-license-faq and akka.io/pricing — **the revenue threshold and the terms may have changed since 2022**; check with legal. Status and releases of **Apache Pekko** at pekko.apache.org (not just GitHub).1765. **Effect stacks**: releases of cats-effect/fs2/http4s/doobie and of ZIO; check whether http4s 1.0 or doobie 1.0 finals have shipped and whether ZIO published a new line.1776. **Tools**: scalafmt, scalafix, munit, ScalaCheck, MiMa, testcontainers-scala — latest version **and licence** (licence changes and projects in maintenance mode are a real risk to the catalogue).1787. **CVEs** in the tree (jackson, netty, JDBC drivers, logging libraries) in the projects' advisories and NVD before freezing versions.1798. **Declared gap, unverified as of Aug 2026**: the formal EOL date of the Scala 2.13 line and of 3.3 LTS (no published date located, only the relative guarantee "≥1 year after the next LTS"); ScalaTest's maintenance status (no releases since 2024-06, no official end-of-support statement found); Akka's exact current commercial terms.180181If the web contradicts this document, **the web wins** — flag the discrepancy.