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 theimport-buildtool - to lookup a dependency or the latest version, use the
find-deptool. Iffind-depis unavailable, resolve versions yourself — but NEVER fromsearch.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 was3.3.0-SNAP4, Scala 3's3.9.0-RC1); unless you specifically want one, skip versions containingRC,M<n>,SNAP,alpha,beta, orNIGHTLYand 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.
- latest version of a KNOWN artifact — read the canonical resolver source,
- to lookup the API of a class, use the
inspecttool. To lookup the docs or usages, use theget-docsandget-usagestools - to compile the project, use
compile-full,compile-moduletools - to search for symbols, use
glob-searchandtyped-glob-searchtools - if you do need to use
sbt, usesbt --clientinstead ofsbtto 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
scalafmtplugin:sbt --client scalafmtAll - the project MUST compile with zero warnings. Ensure
build.sbtincludes-Wunused:all,-Wvalue-discard,-Wnonunit-statementinscalacOptions. Fix warnings in code; only use@nowarnfor 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
.scalafile, 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>], orprivate[<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
.pipeand.tap(import ox.*) to drop single-usevals that only feed the next line..pipe(f)returnsf(value);.tap(f)runs a side effect and returns the value unchanged. Keep a namedvalwhen 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
Flowor paging to process large datasets and paginated API results incrementally.
Direct-style Scala
- in Tapir, use
.handle/.handleSecurity/.handleSuccessto wire endpoint logic — NEVER use.serverLogic/.serverSecurityLogic. The.handlefamily is the direct-style API. The.serverLogicfamily 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. Usejava.util.concurrentcoordination primitives only for pure atomic state or when bridging a foreign API that Ox does not cover. - create local, focused
supervisedscopes 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.
Functional programming
- use pure functions, immutable data, higher-order functions, ADTs. NEVER use shared mutable state.
vardeclarations MUST be inside methods (e.g. processing loops), never as class fields. Class-levelvars break reasoning and testability. The sole exception is mutable state encapsulated by an OxActor, 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) — nevermutable.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 thevarthat 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, orUUIDinside 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, andBooleandomain 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 rawStringfields, introduce opaque types at the boundary where generated types are converted to domain types. - eliminate boolean blindness — replace
Booleanparameters 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 usenullor sentinel values.Optionis for presence/absence only, not for errors. - model different states of an entity as separate types — NEVER use
Optionfields 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/catchfor recoverable failures. Reservetry/catchfor 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, requiredscalacOptions,OxApp.Simpleentry 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.Background Processes —
OxAppentry point,forkDiscard/forkUserDiscardfor daemon vs. user threads,forever/sleepfor periodic loops, orderly shutdown.Type-Safe Configuration — PureConfig with
derives ConfigReader, environment variable overrides,Sensitivewrapper, load-time validation.Compile-Time Dependency Injection — MacWire
autowire,autowireMembersOffor config extraction,wireListfor 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.
Error Handling
Error Handling —
FailADT, Oxeitherblocks 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,defaultHandlersfor decode failures and 404s.Decode Failure Handling —
DefaultDecodeFailureHandlercustomisation: respond/message/response pipeline,onDecodeFailureNextEndpoint, custom failure messages,hideEndpointsWithAuth.
HTTP & Endpoints
Authentication —
secureEndpoint[T],AuthTokenOps[T]trait,Auth[T]authenticator,handleSecuritywiring.HTTP Server Configuration — Security headers, CORS, serving static files for SPAs, request cancellation,
NettySyncServerstartup.Version API —
sbt-buildinfogeneratingBuildInfowith 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,@maingenerator, 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.derivedEnumerationfor enum-valued inputs, and how multiple inputs reach the handler.
Data & Integration
SQL Persistence — Magnum with PostgreSQL:
@Tablecase classes,DbCodecfor opaque types,Repo/TableInfo,sqlinterpolation, Flyway migrations, HikariCP.Sending Emails —
EmailSchedulertrait, pluggable senders (SMTP, Mailgun, dummy), email templates, background batch processing.Kafka Streaming —
KafkaFlow.subscribe,mapPar,KafkaDrainpublishing, offset commits, transactional produce-and-commit, graceful shutdown.
Testing & Observability
Testing HTTP Endpoints —
TapirSyncStubInterpreterstub backend,SttpClientInterpreterfor type-safe requests, testing public and secured endpoints in-process.OpenTelemetry Observability — SDK auto-configuration, Tapir tracing/metrics interceptors, sttp client instrumentation, custom metrics,
PropagatingVirtualThreadFactoryfor context propagation, MDC log correlation.