| name | direct-style-scala |
| description | Scala coding style, tooling, and functional programming guidance, with dedicated sections on direct-style Scala, Ox structured concurrency, and synchronous Tapir. Auto-load for any task involving Scala code, especially when using direct-style or "plain" Scala. |
You are an expert backend software engineer and architect.
Scala tooling
- ALWAYS use tools to compile and run tests instead of relying on bash commands
- after adding a dependency to
build.sbt, ALWAYS run the import-build tool
- to lookup a dependency or the latest version, use the
find-dep tool. If
find-dep is unavailable, resolve versions yourself — but NEVER from
search.maven.org/solrsearch, whose index can be stale by many months (it has
been observed pinned to year-old versions). Use these instead:
- latest version of a KNOWN artifact — read the canonical resolver source,
https://repo1.maven.org/maven2/<group-with-slashes>/<artifact>/maven-metadata.xml,
and take <release> (or the last <version>). <release> may itself be a
prerelease (e.g. scalatest's was 3.3.0-SNAP4, Scala 3's 3.9.0-RC1); unless
you specifically want one, skip versions containing RC, M<n>, SNAP,
alpha, beta, or NIGHTLY and take the latest stable. Remember the Scala
suffix, e.g. com/softwaremill/ox/core_3. This is never stale.
- DISCOVERY by name (unknown coordinates) — query Scaladex, which is
Scala-aware (handles
_3 / cross-versions):
https://index.scala-lang.org/api/autocomplete?q=<name> to find the
org/repo, then look up the exact artifact on repo1 as above.
- to lookup the API of a class, use the
inspect tool. To lookup the docs or
usages, use the get-docs and get-usages tools
- to compile the project, use
compile-full, compile-module tools
- to search for symbols, use
glob-search and typed-glob-search tools
- if you do need to use
sbt, use sbt --client instead of sbt to connect to
a running sbt server for faster execution
- to verify that the app starts use
sbt run, WITHOUT --client, as it
prevents interrupting the process
- before committing, ALWAYS format all changed Scala files using the sbt
scalafmt plugin: sbt --client scalafmtAll
- the project MUST compile with zero warnings. Ensure
build.sbt includes
-Wunused:all, -Wvalue-discard, -Wnonunit-statement in scalacOptions.
Fix warnings in code; only use @nowarn for generated code or unfixable
third-party issues (with a comment explaining why).
Coding style
- ALWAYS use braceless syntax — do not use
{}
- responsibilities in code MUST be segregated between appropriately named
entities
- before creating or moving a
.scala file, decide its package, filename, and
top-level visibility. Read Code Organization and
Visibility when adding packages/modules or
widening visibility.
- when dealing with resources, properly track who owns which resources, and
ensure proper ordering on cleanup
- every top-level class, trait, enum, and object MUST have intentional
visibility at declaration time: default-public (no modifier),
private[<subpkg>], or private[<rootpkg>]. Choose by scanning actual call
sites.
- comment on any aspects that aren't obvious from the implementation, but are
important to know when reading the code
- each function MUST handle exactly one concern — either a single logical
operation, or a short orchestration of named steps. When a function does
multiple things (validate, transform, persist, notify), extract each step into
its own well-named function so the orchestrator reads as a sequence of
intentions. Naming a coherent step is always a valid reason to extract, even
if the logic is used only once.
- use Ox's
.pipe and .tap (import ox.*) to drop single-use vals that
only feed the next line. .pipe(f) returns f(value); .tap(f) runs a side
effect and returns the value unchanged. Keep a named val when the name
documents intent or the value is reused
- tests MUST be targeted — each test covers exactly one scenario. No
overlapping or redundant tests.
- every public function, val, and given MUST have an explicit return type — this
prevents accidental signature drift during refactors:
// Wrong — inferred return type can silently change:
def findUser(id: Id[User])(using DbTx) =
userModel.findById(id).toRight(Fail.NotFound("user"))
// Right — return type is explicit and stable:
def findUser(id: Id[User])(using DbTx): Either[Fail, User] =
userModel.findById(id).toRight(Fail.NotFound("user"))
Performance
- NEVER materialize unbounded data into memory. Use streaming with
Flow or
paging to process large datasets and paginated API results incrementally.
- virtual threads are never preempted — long CPU-bound computations (a few
suffice, e.g. a
mapPar over such work) can starve every other virtual
thread in the process. Run long or non-instrumentable compute via
computeIntensive (platform-thread pool; the blocking caller keeps it
structured); in CPU-bound loops you control, call cede() about once per
millisecond.
Direct-style Scala
- in Tapir, use
.handle / .handleSecurity / .handleSuccess to wire
endpoint logic — NEVER use .serverLogic / .serverSecurityLogic. The
.handle family is the direct-style API. The .serverLogic family requires
a monadic wrapper (Future, IO) and MUST NOT be used.
- ALWAYS use Ox for threading, channels, and async coordination. Avoid raw
Thread.ofVirtual, LinkedBlockingQueue, synchronized/Lock, and
lifecycle flags. Use java.util.concurrent coordination primitives only for
pure atomic state or when bridging a foreign API that Ox does not cover.
- create local, focused
supervised scopes for request-, message-, or
job-level concurrency. Accept a parent (using Ox) only when a fork or
resource must be tied to that parent scope's lifetime.
- keep constructors plain; use factories that take
(using Ox) and return
values that do not carry the capability. If the factory only registers
resources and starts no forks, take the narrower (using ResourceScope);
for scoped cleanup with no enclosing scope and no concurrency, use
resourceScope instead of supervised.
- decide the owning scope BEFORE writing concurrent code. Model a stream reader
or worker as a fork in a
supervised scope whose lifetime matches the work;
its result is the fork's return value (fork{…}.join()), not a value
published through a shared AtomicReference. NEVER return an object that owns
running forks/threads to be driven later — its lifetime escapes every scope,
making cancellation and cleanup manual again. Pass a consumer into the scope
instead of handing a live handle out.
- a fork blocked reading a subprocess pipe, stdin, a file, or any other
classic
java.io stream is NOT ended by scope cancellation — without the
right teardown shape, shutdown deadlocks. BEFORE writing code that drives a
subprocess or reads a blocking external stream (socket, SSE), read
Subprocesses and External
Streams.
Functional programming
- use pure functions, immutable data, higher-order functions, ADTs. NEVER use
shared mutable state.
var declarations MUST be inside methods (e.g. processing loops), never
as class fields. Class-level vars break reasoning and testability. The sole
exception is mutable state encapsulated by an Ox Actor, which serialises every
invocation onto a single thread — the actor is what makes the field safe to hold
(see Concurrency and Inter-Thread Communication).
Use only immutable collections (Map, Set, List) — never mutable.Map,
mutable.Set, mutable.Buffer.
- model state as an immutable case class. State transitions are pure functions
that take the current state and return a new one via
.copy(). Confine the
var that threads state to the smallest possible scope:
case class ProcessingState(
processed: Map[String, Long] = Map.empty,
pending: Set[String] = Set.empty
)
def handleItem(state: ProcessingState, item: Item): ProcessingState =
state.copy(processed = state.processed.updated(item.key, item.offset))
def run(items: Iterator[Item]): ProcessingState =
var state = ProcessingState()
for item <- items do
state = handleItem(state, item)
state
- push side effects behind traits so that state transitions are testable without
real infrastructure. Tests substitute in-memory implementations — mutable
collections are acceptable in test helpers that simulate external systems.
- APIs MUST be lawful: given identical arguments and explicit dependencies,
they yield the same observable result. Do not hide dependencies like
Clock,
Random, or UUID inside methods — pass them explicitly or capture them in
the class constructor:
// Wrong — hidden non-determinism:
class OrderService:
def place(order: Order): Confirmation =
val id = UUID.randomUUID()
val now = Instant.now()
Confirmation(id, now)
// Right — dependencies are explicit and injectable:
class OrderService(clock: Clock, idGenerator: () => UUID):
def place(order: Order): Confirmation =
val id = idGenerator()
val now = clock.instant()
Confirmation(id, now)
- wrap
String, Int, Long, and Boolean domain values in opaque types or
enums — NEVER use raw primitives for domain concepts. This applies to
identifiers (OrderId, ProductCode), quantities (Quantity, Amount), and
configuration values (Port, TopicName). When a generated library (e.g.
scalaxb) produces raw String fields, introduce opaque types at the boundary
where generated types are converted to domain types.
- eliminate boolean blindness — replace
Boolean parameters and return values
with two-case enums so intent is explicit and exhaustiveness is checked:
// Wrong — caller must remember what `true` means:
def recordFlush(success: Boolean, durationMs: Double): Unit
// Right — intent is unambiguous:
enum FlushOutcome:
case Success, Failure
def recordFlush(outcome: FlushOutcome, duration: Duration): Unit
- NEVER throw exceptions for recoverable failures. Instead, return an
Either[E, T].
Use exceptions only for unrecoverable errors, which should terminate the current
processing unit (request, message handling, etc.)
- if a value can be absent, use
Option[T] — NEVER use null or sentinel
values. Option is for presence/absence only, not for errors.
- model different states of an entity as separate types — NEVER use
Option
fields to represent state transitions:
// Wrong — callers must remember to check confirmedAt:
case class Order(id: Id[Order], items: List[Item], confirmedAt: Option[Instant])
// Right — the type tells you what state the order is in:
case class PendingOrder(id: Id[Order], items: List[Item])
case class ConfirmedOrder(id: Id[Order], items: List[Item], confirmedAt: Instant)
- design domain models so that invalid data CANNOT be constructed. Use enums,
opaque types, or smart constructors to encode invariants:
// Wrong — any string is accepted:
def setPort(port: Int): Unit
// Right — invalid values are rejected at construction:
opaque type Port = Int
object Port:
def apply(value: Int): Either[String, Port] =
if value >= 1 && value <= 65535 then Right(value)
else Left(s"Port out of range: $value")
- define sealed-trait or enum error hierarchies — NEVER use stringly-typed
errors.
- NEVER use bare
try/catch for recoverable failures. Reserve try/catch for
defect or unrecoverable error boundaries only.
Use-Case Guide
BEFORE writing any code that uses Tapir, Ox, sttp, or direct-style Scala, you MUST
fetch the chapter(s) relevant to your current task from this guide and follow the
patterns shown there. This is not optional — code that ignores guide patterns will
be rejected in review.
Retrieve the chapter as raw, unmodified text — read every code block and
paragraph in full. Do NOT use a tool that summarises the page: summaries silently
drop the > Required / > Important callouts and the exact API calls that make
the chapter correct. Prefer reading the chapter file directly from the installed
skill directory; if fetching over the network, use a method that returns the
verbatim file (a raw HTTP GET), not a fetch-and-summarise tool.
Every API, pattern, and constraint described in the fetched chapter MUST be
followed. If the chapter says to use a specific API (e.g. useInScope for
resource management), do NOT substitute a different approach. If the chapter
marks something as required, it is required.
To fetch a chapter, use the base URL below followed by the chapter filename
listed in the index that follows:
https://raw.githubusercontent.com/virtuslab/scala-skill/refs/heads/master/direct-style-scala/skills/direct-style-scala/
Application Structure
-
New Project Setup — minimal direct-style
Scala project skeleton with sbt and Ox: directory layout, build.sbt,
required scalacOptions, OxApp.Simple entry point. adopt-tapir as a
starting point for HTTP projects.
-
Code Organization and Visibility — top-level
visibility, file naming exceptions, Scala 3 package shadowing, and
sbt/Scalafix boundary enforcement.
-
Resource Management — useInScope,
useCloseableInScope, reverse-order release, scope-based cleanup;
resourceScope for cleanup without concurrency, using ResourceScope as
the narrower capability.
-
Background Processes — OxApp entry point,
forkDiscard/forkUserDiscard for daemon vs. user threads,
forever/sleep for periodic loops, orderly shutdown.
-
Type-Safe Configuration — PureConfig with
derives ConfigReader, environment variable overrides, Sensitive wrapper,
load-time validation.
-
Compile-Time Dependency Injection
— MacWire autowire, autowireMembersOf for config extraction, wireList
for collecting endpoints.
-
Concurrency and Inter-Thread Communication
— Flows for declarative concurrent pipelines (mapPar, merge,
mapStateful), Ox primitive selection, channels for worker mailboxes and
shutdown, actors for serialized mutable state, computeIntensive/cede for
CPU-bound work on virtual threads.
-
Subprocesses and External Streams
— driving a subprocess / socket / SSE reader as a fork whose return value is
the result; why a non-interruptible pipe read needs the resource destroyed
in the scope body's finally (before the join) rather than via
releaseAfterScope; process-tree teardown; abandonOnInterruptReads for
reads that can't be unblocked by closing; pipe back-pressure.
Error Handling
-
Error Handling — Fail ADT, Ox either blocks with
.ok() short-circuiting, transactEither, .catching, nesting rules.
-
Error Output Customisation — JSON error
responses for all error types. Bidirectional Fail → HTTP status code
mapping, failOutput, defaultHandlers for decode failures and 404s.
-
Decode Failure Handling —
DefaultDecodeFailureHandler customisation: respond/message/response pipeline,
onDecodeFailureNextEndpoint, custom failure messages,
hideEndpointsWithAuth.
HTTP & Endpoints
-
Authentication — secureEndpoint[T],
AuthTokenOps[T] trait, Auth[T] authenticator, handleSecurity wiring.
-
HTTP Server Configuration — Security
headers, CORS, serving static files for SPAs, request cancellation,
NettySyncServer startup.
-
Version API — sbt-buildinfo generating BuildInfo
with git commit hash, served from a Tapir endpoint.
-
Compile-Time OpenAPI Generation —
Build-time OpenAPI YAML generation for frontend client codegen (not runtime
Swagger UI). EndpointsForDocs, @main generator, sbt task wiring.
-
SOAP with scalaxb — XSD-to-Scala code generation,
SOAP envelope wrapping/unwrapping, Tapir XML codecs for scalaxb types,
SOAPAction-based endpoint routing, SOAP fault error handlers.
-
JSON Request and Response Bodies — jsoniter codec
derivation for DTOs, why list bodies need their own codec, encoding
parameterless enums as plain strings via withDiscriminatorFieldName(None),
and using opaque-type identifiers directly in DTOs.
-
Endpoint Inputs — PlainCodecs for
path/query/header inputs: mapping a built-in codec onto an opaque-type id,
Codec.derivedEnumeration for enum-valued inputs, and how multiple inputs reach
the handler.
Data & Integration
-
SQL Persistence — Magnum with PostgreSQL: @Table
case classes, DbCodec for opaque types, Repo/TableInfo, sql
interpolation, Flyway migrations, HikariCP.
-
Sending Emails — EmailScheduler trait, pluggable
senders (SMTP, Mailgun, dummy), email templates, background batch processing.
-
Kafka Streaming — KafkaFlow.subscribe, mapPar,
KafkaDrain publishing, offset commits, transactional produce-and-commit,
graceful shutdown.
Testing & Observability
-
Testing HTTP Endpoints —
TapirSyncStubInterpreter stub backend, SttpClientInterpreter for
type-safe requests, testing public and secured endpoints in-process.
-
OpenTelemetry Observability — SDK
auto-configuration, Tapir tracing/metrics interceptors, sttp client
instrumentation, custom metrics, PropagatingVirtualThreadFactory for
context propagation, MDC log correlation.