direct-style-scala

star 59

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.

VirtusLab By VirtusLab schedule Updated 5/28/2026

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.

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. 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 ManagementuseInScope, useCloseableInScope, reverse-order release, scope-based cleanup.

  • Background ProcessesOxApp 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 HandlingFail 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 HandlingDefaultDecodeFailureHandler customisation: respond/message/response pipeline, onDecodeFailureNextEndpoint, custom failure messages, hideEndpointsWithAuth.

HTTP & Endpoints

  • AuthenticationsecureEndpoint[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 APIsbt-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 InputsPlainCodecs 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 EmailsEmailScheduler trait, pluggable senders (SMTP, Mailgun, dummy), email templates, background batch processing.

  • Kafka StreamingKafkaFlow.subscribe, mapPar, KafkaDrain publishing, offset commits, transactional produce-and-commit, graceful shutdown.

Testing & Observability

  • Testing HTTP EndpointsTapirSyncStubInterpreter 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.

Install via CLI
npx skills add https://github.com/VirtusLab/scala-skill --skill direct-style-scala
Repository Details
star Stars 59
call_split Forks 3
navigation Branch main
article Path SKILL.md
More from Creator