direct-style-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 - 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
- when dealing with resources, properly track who owns which resources, and ensure proper ordering on cleanup
- restrict visibility of classes and top-level constructs to appropriate sub-packages when possible. No need to restrict visibility to the main package.
- multiple classes in one file is acceptable when they are tightly coupled
- 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
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. - only propagate
(using Ox)when the method genuinely needs to start forks or register resources in the caller's scope. Otherwise, create a localsupervisedblock.
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. 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.
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/
Application Structure
-
Resource Management —
useInScope,useCloseableInScope, reverse-order release, scope-based cleanup. -
Background Processes —
OxAppentry point,fork/forkUserfor 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), channels for imperative send/receive, 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.
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.