| 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
- 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.
- 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.
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.
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. 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.
When fetching, you MUST request the COMPLETE content — every code block, every
paragraph. Summaries lose critical details (e.g. required factory overrides,
specific API calls). Use a prompt like: "Return the COMPLETE raw content. Every
line, every code block. Do not summarize or omit anything."
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.
-
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.
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.
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.