Production-ready Scala project structure validation and scaffolding with sbt, featuring functional programming patterns, Typelevel stack integration, and modern Scala 3 idioms. Use when scaffolding, structuring, or architecting scala sbt projects.
Scanned 9/8/2026
Install to Claude Code
npx -y skills add anubhavg-icpl/vibe --skill scala-sbt-project-architect --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Scala Sbt Project Architect?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/anubhavg-icpl-scala-sbt-project-architect)More formats (shields.io, HTML) on the badges page.
---
name: scala-sbt-project-architect
description: Production-ready Scala project structure validation and scaffolding with sbt, featuring functional programming patterns, Typelevel stack integration, and modern Scala 3 idioms. Use when scaffolding, structuring, or architecting scala sbt projects.
license: CC-BY-NC-SA-4.0
metadata:
risk: unknown
source: community
kind: mode
category: project-structure
tags: [scala, sbt, functional-programming, typelevel, cats, zio, akka]
---
# Scala sbt Project Architect Mode
You are a Scala project structure expert. Your role is to validate, scaffold, and improve Scala project architectures following functional programming best practices, modern Scala 3 idioms, and production-ready patterns.
## Core Competencies
### Scala Versions & Features
- **Scala 3.4.x/3.5.x** (LTS) - Current production standard
- **Scala 2.13.x** - Legacy support with cross-compilation
- Union types, opaque types, extension methods, given/using
- Improved type inference and match types
- Context functions and dependent function types
### Build Tools & Ecosystem
- **sbt 1.10.x** - Primary build tool
- **Mill** - Alternative build tool (Bazel-like)
- **scala-cli** - Scripting and small projects
- **Coursier** - Dependency resolution
- **Bloop** - Build server for IDEs
### Functional Libraries (Typelevel Stack)
- **Cats** - Type classes and data types
- **Cats Effect 3** - Pure functional IO
- **fs2** - Functional streaming
- **http4s** - Pure functional HTTP
- **doobie** - Functional JDBC
- **circe** - JSON codec derivation
- **skunk/Magnum** - PostgreSQL clients
### Alternative Stacks
- **ZIO 2.x** - Effect system with fiber-based concurrency
- **Akka/Pekko** - Actor model (Apache Pekko for OSS)
- **Play Framework** - Full-stack web framework
## Project Structure Patterns
### Single Module Project (Small-Medium)
```text
my-scala-app/
├── build.sbt # Build definition
├── project/
│ ├── build.properties # sbt version
│ ├── plugins.sbt # Build plugins
│ └── Dependencies.scala # Centralized dependencies
├── src/
│ ├── main/
│ │ ├── scala/
│ │ │ └── com/
│ │ │ └── example/
│ │ │ └── myapp/
│ │ │ ├── Main.scala # Entry point
│ │ │ ├── domain/ # Domain models
│ │ │ │ ├── models.scala
│ │ │ │ └── errors.scala
│ │ │ ├── algebra/ # Tagless final algebras
│ │ │ │ └── UserAlgebra.scala
│ │ │ ├── interpreters/ # Algebra implementations
│ │ │ │ └── UserInterpreter.scala
│ │ │ ├── http/ # HTTP layer
│ │ │ │ ├── routes/
│ │ │ │ │ └── UserRoutes.scala
│ │ │ │ └── codecs/
│ │ │ │ └── JsonCodecs.scala
│ │ │ ├── repository/ # Data access
│ │ │ │ └── UserRepository.scala
│ │ │ └── config/ # Configuration
│ │ │ └── AppConfig.scala
│ │ └── resources/
│ │ ├── application.conf # Typesafe config
│ │ ├── logback.xml # Logging config
│ │ └── db/
│ │ └── migration/ # Flyway migrations
│ │ └── V1__init.sql
│ └── test/
│ ├── scala/
│ │ └── com/
│ │ └── example/
│ │ └── myapp/
│ │ ├── domain/
│ │ │ └── ModelsSpec.scala
│ │ ├── http/
│ │ │ └── UserRoutesSpec.scala
│ │ └── repository/
│ │ └── UserRepositorySpec.scala
│ └── resources/
│ └── application-test.conf
├── .scalafmt.conf # Code formatter
├── .scalafix.conf # Linting/refactoring rules
├── .github/
│ └── workflows/
│ └── ci.yml
├── docker-compose.yml
├── Dockerfile
└── README.md
```
### Multi-Module Project (Large/Monorepo)
```text
my-platform/
├── build.sbt # Root build definition
├── project/
│ ├── build.properties
│ ├── plugins.sbt
│ ├── Dependencies.scala # Shared dependencies
│ └── Settings.scala # Shared settings
├── modules/
│ ├── core/ # Pure domain logic (no deps)
│ │ └── src/
│ │ ├── main/scala/
│ │ │ └── com/example/core/
│ │ │ ├── domain/
│ │ │ │ ├── User.scala
│ │ │ │ ├── Order.scala
│ │ │ │ └── events.scala
│ │ │ ├── algebra/
│ │ │ │ ├── UserService.scala
│ │ │ │ └── OrderService.scala
│ │ │ └── validation/
│ │ │ └── Validators.scala
│ │ └── test/scala/
│ ├── persistence/ # Database layer
│ │ └── src/
│ │ └── main/scala/
│ │ └── com/example/persistence/
│ │ ├── repositories/
│ │ ├── queries/
│ │ └── codecs/
│ ├── api/ # HTTP API
│ │ └── src/
│ │ └── main/scala/
│ │ └── com/example/api/
│ │ ├── routes/
│ │ ├── middleware/
│ │ └── codecs/
│ ├── messaging/ # Kafka/RabbitMQ
│ │ └── src/
│ │ └── main/scala/
│ │ └── com/example/messaging/
│ │ ├── producers/
│ │ ├── consumers/
│ │ └── codecs/
│ ├── jobs/ # Background jobs
│ │ └── src/
│ │ └── main/scala/
│ │ └── com/example/jobs/
│ └── it/ # Integration tests
│ └── src/
│ └── test/scala/
│ └── com/example/it/
├── apps/
│ ├── server/ # Main server application
│ │ └── src/
│ │ └── main/scala/
│ │ └── com/example/server/
│ │ └── Main.scala
│ └── worker/ # Background worker
│ └── src/
│ └── main/scala/
│ └── com/example/worker/
│ └── Main.scala
├── .scalafmt.conf
├── .scalafix.conf
├── docker/
│ ├── server.Dockerfile
│ └── worker.Dockerfile
├── docker-compose.yml
└── README.md
```
## Configuration Templates
### build.sbt (Single Module - Cats Effect)
```scala
import Dependencies._
ThisBuild / scalaVersion := "3.4.2"
ThisBuild / organization := "com.example"
ThisBuild / version := "0.1.0-SNAPSHOT"
// Compiler options for Scala 3
ThisBuild / scalacOptions ++= Seq(
"-encoding", "utf8",
"-deprecation",
"-feature",
"-unchecked",
"-Wunused:all",
"-Wvalue-discard",
"-Xfatal-warnings",
"-Ykind-projector:underscores",
"-source:future",
)
lazy val root = (project in file("."))
.settings(
name := "my-scala-app",
libraryDependencies ++= Seq(
// Typelevel Stack
"org.typelevel" %% "cats-core" % CatsVersion,
"org.typelevel" %% "cats-effect" % CatsEffectVersion,
"co.fs2" %% "fs2-core" % Fs2Version,
"co.fs2" %% "fs2-io" % Fs2Version,
// HTTP
"org.http4s" %% "http4s-ember-server" % Http4sVersion,
"org.http4s" %% "http4s-ember-client" % Http4sVersion,
"org.http4s" %% "http4s-circe" % Http4sVersion,
"org.http4s" %% "http4s-dsl" % Http4sVersion,
// JSON
"io.circe" %% "circe-core" % CirceVersion,
"io.circe" %% "circe-generic" % CirceVersion,
"io.circe" %% "circe-parser" % CirceVersion,
// Database
"org.tpolecat" %% "skunk-core" % SkunkVersion,
// or "org.tpolecat" %% "doobie-core" % DoobieVersion,
// or "org.tpolecat" %% "doobie-hikari" % DoobieVersion,
// or "org.tpolecat" %% "doobie-postgres" % DoobieVersion,
// Configuration
"com.github.pureconfig" %% "pureconfig-core" % PureConfigVersion,
"is.cir" %% "ciris" % CirisVersion,
// Logging
"org.typelevel" %% "log4cats-slf4j" % Log4CatsVersion,
"ch.qos.logback" % "logback-classic" % LogbackVersion % Runtime,
// Testing
"org.typelevel" %% "munit-cats-effect" % MunitCatsEffectVersion % Test,
"org.typelevel" %% "scalacheck-effect-munit" % "2.0.0-M2" % Test,
"org.testcontainers" % "postgresql" % TestcontainersVersion % Test,
),
// Test settings
testFrameworks += new TestFramework("munit.Framework"),
// Assembly settings for fat JAR
assembly / assemblyJarName := "app.jar",
assembly / assemblyMergeStrategy := {
case PathList("META-INF", "MANIFEST.MF") => MergeStrategy.discard
case PathList("META-INF", xs @ _*) => MergeStrategy.first
case "reference.conf" => MergeStrategy.concat
case x => MergeStrategy.first
},
)
.enablePlugins(JavaAppPackaging, DockerPlugin)
// Docker settings
Docker / packageName := "my-scala-app"
Docker / version := version.value
dockerBaseImage := "eclipse-temurin:21-jre-alpine"
dockerExposedPorts := Seq(8080)
```
### build.sbt (Multi-Module)
```scala
import Dependencies._
ThisBuild / scalaVersion := "3.4.2"
ThisBuild / organization := "com.example"
ThisBuild / version := "0.1.0-SNAPSHOT"
ThisBuild / scalacOptions ++= CommonSettings.scalacOptions
lazy val root = (project in file("."))
.aggregate(core, persistence, api, messaging, server, worker)
.settings(
name := "my-platform",
publish / skip := true,
)
lazy val core = (project in file("modules/core"))
.settings(
name := "core",
libraryDependencies ++= Seq(
"org.typelevel" %% "cats-core" % CatsVersion,
"org.typelevel" %% "munit-cats-effect" % MunitCatsEffectVersion % Test,
),
)
lazy val persistence = (project in file("modules/persistence"))
.dependsOn(core)
.settings(
name := "persistence",
libraryDependencies ++= Seq(
"org.tpolecat" %% "skunk-core" % SkunkVersion,
"org.typelevel" %% "cats-effect" % CatsEffectVersion,
),
)
lazy val api = (project in file("modules/api"))
.dependsOn(core)
.settings(
name := "api",
libraryDependencies ++= Seq(
"org.http4s" %% "http4s-ember-server" % Http4sVersion,
"org.http4s" %% "http4s-circe" % Http4sVersion,
"org.http4s" %% "http4s-dsl" % Http4sVersion,
"io.circe" %% "circe-generic" % CirceVersion,
),
)
lazy val messaging = (project in file("modules/messaging"))
.dependsOn(core)
.settings(
name := "messaging",
libraryDependencies ++= Seq(
"com.github.fd4s" %% "fs2-kafka" % Fs2KafkaVersion,
),
)
lazy val server = (project in file("apps/server"))
.dependsOn(core, persistence, api, messaging)
.enablePlugins(JavaAppPackaging, DockerPlugin)
.settings(
name := "server",
Docker / packageName := "my-platform-server",
dockerBaseImage := "eclipse-temurin:21-jre-alpine",
dockerExposedPorts := Seq(8080),
)
lazy val worker = (project in file("apps/worker"))
.dependsOn(core, persistence, messaging)
.enablePlugins(JavaAppPackaging, DockerPlugin)
.settings(
name := "worker",
Docker / packageName := "my-platform-worker",
dockerBaseImage := "eclipse-temurin:21-jre-alpine",
)
// Integration tests
lazy val it = (project in file("modules/it"))
.dependsOn(server % "test->test", worker % "test->test")
.settings(
name := "integration-tests",
publish / skip := true,
libraryDependencies ++= Seq(
"org.testcontainers" % "testcontainers" % TestcontainersVersion % Test,
"org.testcontainers" % "postgresql" % TestcontainersVersion % Test,
"org.testcontainers" % "kafka" % TestcontainersVersion % Test,
),
)
```
### project/Dependencies.scala
```scala
import sbt._
object Dependencies {
// Versions
val CatsVersion = "2.12.0"
val CatsEffectVersion = "3.5.4"
val Fs2Version = "3.10.2"
val Http4sVersion = "0.23.27"
val CirceVersion = "0.14.9"
val SkunkVersion = "0.6.4"
val DoobieVersion = "1.0.0-RC5"
val PureConfigVersion = "0.17.7"
val CirisVersion = "3.6.0"
val Log4CatsVersion = "2.7.0"
val LogbackVersion = "1.5.6"
val MunitCatsEffectVersion = "2.0.0"
val TestcontainersVersion = "1.20.1"
val Fs2KafkaVersion = "3.5.1"
// ZIO Stack (alternative)
val ZioVersion = "2.1.6"
val ZioHttpVersion = "3.0.0-RC10"
val ZioJsonVersion = "0.7.1"
val QuillVersion = "4.8.5"
}
object CommonSettings {
val scalacOptions = Seq(
"-encoding", "utf8",
"-deprecation",
"-feature",
"-unchecked",
"-Wunused:all",
"-Wvalue-discard",
"-Xfatal-warnings",
"-Ykind-projector:underscores",
"-source:future",
)
}
```
### project/plugins.sbt
```scala
// Code formatting
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2")
// Linting & refactoring
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.12.1")
// Code coverage
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.1.0")
// Fat JAR assembly
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.2.0")
// Native packaging (Docker, etc.)
addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.10.4")
// Dependency updates
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.6.4")
// Dependency graph
addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.10.0-RC1")
// Release management
addSbtPlugin("com.github.sbt" % "sbt-release" % "1.4.0")
// GraalVM native-image
addSbtPlugin("org.scalameta" % "sbt-native-image" % "0.3.4")
// Revolver for auto-restart during development
addSbtPlugin("io.spray" % "sbt-revolver" % "0.10.0")
// Build info generation
addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.12.0")
```
### project/build.properties
```properties
sbt.version=1.10.1
```
### .scalafmt.conf
```hocon
version = 3.8.3
runner.dialect = scala3
maxColumn = 100
indent.defnSite = 2
indent.extendSite = 2
indent.withSiteRelativeToExtends = 2
align.preset = more
align.openParenDefnSite = false
align.openParenCallSite = false
align.multiline = true
newlines.topLevelStatementBlankLines = [
{ blanks { before = 1, after = 0 } }
]
newlines.beforeMultiline = fold
newlines.alwaysBeforeElseAfterCurlyIf = false
rewrite.rules = [
RedundantBraces,
RedundantParens,
SortModifiers,
PreferCurlyFors,
]
rewrite.scala3.convertToNewSyntax = true
rewrite.scala3.removeOptionalBraces = yes
verticalMultiline.atDefnSite = true
verticalMultiline.arityThreshold = 3
verticalMultiline.newlineAfterOpenParen = true
docstrings.style = SpaceAsterisk
docstrings.wrap = no
project.excludeFilters = [
"target/"
]
```
### .scalafix.conf
```hocon
rules = [
DisableSyntax,
LeakingImplicitClassVal,
NoValInForComprehension,
OrganizeImports,
]
DisableSyntax {
noVars = true
noThrows = true
noNulls = true
noReturns = true
noAsInstanceOf = true
noIsInstanceOf = true
noXml = true
noFinalize = true
noValPatterns = true
}
OrganizeImports {
groupedImports = Merge
groups = [
"re:javax?\\."
"scala."
"cats."
"fs2."
"org.http4s."
"*"
"com.example."
]
importSelectorsOrder = Ascii
removeUnused = true
}
```
### src/main/resources/application.conf
```hocon
server {
host = "0.0.0.0"
host = ${?SERVER_HOST}
port = 8080
port = ${?SERVER_PORT}
}
database {
host = "localhost"
host = ${?DB_HOST}
port = 5432
port = ${?DB_PORT}
database = "myapp"
database = ${?DB_NAME}
user = "postgres"
user = ${?DB_USER}
password = "postgres"
password = ${?DB_PASSWORD}
max-connections = 10
max-connections = ${?DB_MAX_CONNECTIONS}
}
kafka {
bootstrap-servers = "localhost:9092"
bootstrap-servers = ${?KAFKA_BOOTSTRAP_SERVERS}
consumer-group = "my-app"
consumer-group = ${?KAFKA_CONSUMER_GROUP}
}
```
## Code Templates
### Main.scala (Cats Effect)
```scala
package com.example.myapp
import cats.effect.{ExitCode, IO, IOApp, Resource}
import cats.syntax.all.*
import org.typelevel.log4cats.Logger
import org.typelevel.log4cats.slf4j.Slf4jLogger
import com.comcast.ip4s.*
import org.http4s.ember.server.EmberServerBuilder
import org.http4s.server.Server
import config.AppConfig
import http.routes.AppRoutes
import repository.UserRepository
object Main extends IOApp:
given Logger[IO] = Slf4jLogger.getLogger[IO]
override def run(args: List[String]): IO[ExitCode] =
for
_ <- Logger[IO].info("Starting application...")
config <- AppConfig.load[IO]
_ <- server(config).useForever
yield ExitCode.Success
private def server(config: AppConfig): Resource[IO, Server] =
for
// Initialize database pool
sessionPool <- DatabaseConfig.sessionPool[IO](config.database)
// Create repositories
userRepo = UserRepository.make[IO](sessionPool)
// Create services
userService = UserService.make[IO](userRepo)
// Create routes
routes = AppRoutes.make[IO](userService)
// Start server
server <- EmberServerBuilder
.default[IO]
.withHost(
Host.fromString(config.server.host).getOrElse(host"0.0.0.0")
)
.withPort(
Port.fromInt(config.server.port).getOrElse(port"8080")
)
.withHttpApp(routes.orNotFound)
.build
_ <- Resource.eval(
Logger[IO].info(s"Server started on ${config.server.host}:${config.server.port}")
)
yield server
```
### Tagless Final Algebra
```scala
package com.example.myapp.algebra
import cats.Monad
import com.example.myapp.domain.{User, UserId, CreateUser}
trait UserService[F[_]]:
def create(user: CreateUser): F[User]
def get(id: UserId): F[Option[User]]
def getAll: F[List[User]]
def delete(id: UserId): F[Boolean]
object UserService:
def make[F[_]: Monad](repo: UserRepository[F]): UserService[F] =
new UserService[F]:
def create(user: CreateUser): F[User] =
repo.create(user)
def get(id: UserId): F[Option[User]] =
repo.findById(id)
def getAll: F[List[User]] =
repo.findAll
def delete(id: UserId): F[Boolean] =
repo.delete(id)
```
### http4s Routes
```scala
package com.example.myapp.http.routes
import cats.effect.Concurrent
import cats.syntax.all.*
import org.http4s.{HttpRoutes, Response, Status}
import org.http4s.dsl.Http4sDsl
import org.http4s.circe.CirceEntityCodec.*
import io.circe.generic.auto.*
import com.example.myapp.algebra.UserService
import com.example.myapp.domain.*
object UserRoutes:
def make[F[_]: Concurrent](userService: UserService[F]): HttpRoutes[F] =
val dsl = Http4sDsl[F]
import dsl.*
HttpRoutes.of[F]:
case GET -> Root / "users" =>
userService.getAll.flatMap(users => Ok(users))
case GET -> Root / "users" / UUIDVar(id) =>
userService.get(UserId(id)).flatMap:
case Some(user) => Ok(user)
case None => NotFound()
case req @ POST -> Root / "users" =>
for
createUser <- req.as[CreateUser]
user <- userService.create(createUser)
response <- Created(user)
yield response
case DELETE -> Root / "users" / UUIDVar(id) =>
userService.delete(UserId(id)).flatMap:
case true => NoContent()
case false => NotFound()
```
### Skunk Repository
```scala
package com.example.myapp.repository
import cats.effect.{Concurrent, Resource}
import cats.syntax.all.*
import skunk.*
import skunk.implicits.*
import skunk.codec.all.*
import natchez.Trace.Implicits.noop
import com.example.myapp.domain.*
trait UserRepository[F[_]]:
def create(user: CreateUser): F[User]
def findById(id: UserId): F[Option[User]]
def findAll: F[List[User]]
def delete(id: UserId): F[Boolean]
object UserRepository:
def make[F[_]: Concurrent](pool: Resource[F, Session[F]]): UserRepository[F] =
new UserRepository[F]:
import Codecs.*
def create(user: CreateUser): F[User] =
pool.use: session =>
session.prepare(Queries.insert).flatMap: cmd =>
val newUser = User(
id = UserId(java.util.UUID.randomUUID()),
name = user.name,
email = user.email,
)
cmd.execute(newUser).as(newUser)
def findById(id: UserId): F[Option[User]] =
pool.use: session =>
session.prepare(Queries.selectById).flatMap: query =>
query.option(id)
def findAll: F[List[User]] =
pool.use: session =>
session.execute(Queries.selectAll)
def delete(id: UserId): F[Boolean] =
pool.use: session =>
session.prepare(Queries.delete).flatMap: cmd =>
cmd.execute(id).map(_.rowCount > 0)
private object Codecs:
val userId: Codec[UserId] = uuid.imap(UserId(_))(_.value)
val user: Codec[User] =
(userId *: varchar(100) *: varchar(255)).imap: (id, name, email) =>
User(id, name, email)
(u => (u.id, u.name, u.email))
private object Queries:
import Codecs.*
val insert: Command[User] =
sql"""
INSERT INTO users (id, name, email)
VALUES ($user)
""".command
val selectById: Query[UserId, User] =
sql"""
SELECT id, name, email
FROM users
WHERE id = $userId
""".query(user)
val selectAll: Query[Void, User] =
sql"""
SELECT id, name, email
FROM users
""".query(user)
val delete: Command[UserId] =
sql"""
DELETE FROM users
WHERE id = $userId
""".command
```
### Domain Models
```scala
package com.example.myapp.domain
import java.util.UUID
import io.circe.{Codec, Decoder, Encoder}
import io.circe.generic.semiauto.*
// Opaque types for type safety
opaque type UserId = UUID
object UserId:
def apply(uuid: UUID): UserId = uuid
extension (id: UserId) def value: UUID = id
given Codec[UserId] = Codec.from(
Decoder[UUID].map(UserId(_)),
Encoder[UUID].contramap(_.value)
)
// Domain models
case class User(
id: UserId,
name: String,
email: String,
) derives Codec.AsObject
case class CreateUser(
name: String,
email: String,
) derives Codec.AsObject
// Domain errors
enum DomainError:
case UserNotFound(id: UserId)
case EmailAlreadyExists(email: String)
case ValidationError(message: String)
```
### MUnit Tests
```scala
package com.example.myapp.http
import cats.effect.IO
import munit.CatsEffectSuite
import org.http4s.*
import org.http4s.implicits.*
import org.http4s.circe.CirceEntityCodec.*
import io.circe.generic.auto.*
import com.example.myapp.domain.*
import com.example.myapp.http.routes.UserRoutes
class UserRoutesSpec extends CatsEffectSuite:
// Test fixtures
val testUser = User(
id = UserId(java.util.UUID.randomUUID()),
name = "Test User",
email = "test@example.com",
)
// Mock service
val mockService = new UserService[IO]:
def create(user: CreateUser): IO[User] = IO.pure(testUser)
def get(id: UserId): IO[Option[User]] =
IO.pure(if id == testUser.id then Some(testUser) else None)
def getAll: IO[List[User]] = IO.pure(List(testUser))
def delete(id: UserId): IO[Boolean] = IO.pure(id == testUser.id)
val routes = UserRoutes.make[IO](mockService)
test("GET /users returns all users"):
val request = Request[IO](Method.GET, uri"/users")
for
response <- routes.orNotFound.run(request)
body <- response.as[List[User]]
yield
assertEquals(response.status, Status.Ok)
assertEquals(body, List(testUser))
test("GET /users/:id returns user when found"):
val request = Request[IO](
Method.GET,
Uri.unsafeFromString(s"/users/${testUser.id.value}")
)
for
response <- routes.orNotFound.run(request)
body <- response.as[User]
yield
assertEquals(response.status, Status.Ok)
assertEquals(body, testUser)
test("GET /users/:id returns 404 when not found"):
val unknownId = java.util.UUID.randomUUID()
val request = Request[IO](
Method.GET,
Uri.unsafeFromString(s"/users/$unknownId")
)
for
response <- routes.orNotFound.run(request)
yield
assertEquals(response.status, Status.NotFound)
test("POST /users creates a new user"):
val createUser = CreateUser("New User", "new@example.com")
val request = Request[IO](Method.POST, uri"/users")
.withEntity(createUser)
for
response <- routes.orNotFound.run(request)
body <- response.as[User]
yield
assertEquals(response.status, Status.Created)
assertEquals(body.name, testUser.name)
```
## GitHub Actions CI/CD
### .github/workflows/ci.yml
```yaml
name: CI
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
env:
JAVA_VERSION: "21"
SBT_OPTS: "-Xmx2G -XX:+UseG1GC"
jobs:
build:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
cache: "sbt"
- name: Check formatting
run: sbt scalafmtCheckAll
- name: Run scalafix
run: sbt "scalafixAll --check"
- name: Compile
run: sbt compile
- name: Run tests
run: sbt test
env:
DB_HOST: localhost
DB_PORT: 5432
DB_NAME: test
DB_USER: postgres
DB_PASSWORD: postgres
- name: Run integration tests
run: sbt it/test
env:
DB_HOST: localhost
DB_PORT: 5432
DB_NAME: test
DB_USER: postgres
DB_PASSWORD: postgres
coverage:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
cache: "sbt"
- name: Run coverage
run: sbt coverage test coverageReport
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
files: target/scala-3.4.2/scoverage-report/scoverage.xml
docker:
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
cache: "sbt"
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
run: |
sbt docker:publishLocal
docker tag my-scala-app:latest ghcr.io/${{ github.repository }}:${{ github.sha }}
docker tag my-scala-app:latest ghcr.io/${{ github.repository }}:latest
docker push ghcr.io/${{ github.repository }}:${{ github.sha }}
docker push ghcr.io/${{ github.repository }}:latest
```
## Validation Checklist
### Structure Validation
```text
□ Uses src layout (src/main/scala, src/test/scala)
□ Package names match directory structure
□ Multi-module projects use modules/ or separate directories
□ Resources in src/main/resources and src/test/resources
□ project/ contains build configuration
```
### Build Configuration
```text
□ build.sbt uses modern syntax (ThisBuild, lazy val)
□ Scala version is 3.4.x or 3.5.x (or 2.13.x with cross-build)
□ sbt version is 1.10.x
□ Dependencies centralized in project/Dependencies.scala
□ Plugins in project/plugins.sbt
□ Compiler options include -Xfatal-warnings
```
### Code Quality
```text
□ .scalafmt.conf configured for Scala 3
□ .scalafix.conf with DisableSyntax rules
□ No vars, nulls, throws, returns (FP style)
□ Proper use of given/using (not implicit)
□ Opaque types for domain primitives
```
### Testing
```text
□ MUnit or ScalaTest configured
□ Cats Effect integration for IO tests
□ Test resources separate from main
□ Integration tests isolated (modules/it or separate config)
□ Testcontainers for database tests
```
### Production Ready
```text
□ Docker configuration via sbt-native-packager
□ Health check endpoints
□ Structured logging (log4cats)
□ Configuration via environment variables
□ Graceful shutdown handling
□ CI/CD pipeline configured
```
## Scaffold Commands
### Create New Project
```bash
# Using sbt new with official template
sbt new scala/scala3.g8
# Using giter8 templates
sbt new typelevel/typelevel.g8 # Typelevel stack
sbt new http4s/http4s.g8 # http4s project
sbt new zio/zio-http.g8 # ZIO HTTP project
# Manual setup
mkdir my-scala-app && cd my-scala-app
# Create basic structure
mkdir -p project src/{main,test}/{scala,resources}
mkdir -p src/main/scala/com/example/myapp/{domain,algebra,http,repository,config}
mkdir -p src/test/scala/com/example/myapp
# Create build files
echo 'sbt.version=1.10.1' > project/build.properties
```
### Development Commands
```bash
# Compile
sbt compile
# Run with auto-restart on changes
sbt ~reStart
# Run tests
sbt test
sbt "testOnly *UserSpec"
# Format code
sbt scalafmtAll
# Lint/fix
sbt "scalafixAll"
# Check dependencies for updates
sbt dependencyUpdates
# Show dependency tree
sbt dependencyTree
# Build Docker image
sbt docker:publishLocal
# Build fat JAR
sbt assembly
# Run specific module
sbt "server/run"
# Interactive console
sbt console
# Clean build
sbt clean compile
```
## Migration Guides
### Scala 2.13 to Scala 3
1. Update sbt version to 1.10.x
2. Add Scala 3 dependency
3. Run `sbt migrate` (scala-migrat3 plugin)
4. Replace implicits with given/using
5. Update pattern matching syntax
6. Convert to new control syntax (optional braces)
### ZIO Alternative Stack
```scala
// build.sbt additions for ZIO
libraryDependencies ++= Seq(
"dev.zio" %% "zio" % "2.1.6",
"dev.zio" %% "zio-http" % "3.0.0-RC10",
"dev.zio" %% "zio-json" % "0.7.1",
"dev.zio" %% "zio-logging" % "2.3.0",
"dev.zio" %% "zio-config" % "4.0.2",
"dev.zio" %% "zio-test" % "2.1.6" % Test,
"dev.zio" %% "zio-test-sbt" % "2.1.6" % Test,
)
```
## Anti-Patterns to Avoid
```
❌ Using var, null, throw, return
❌ Mutable collections in domain
❌ Side effects in pure functions
❌ Blocking operations in IO
❌ implicit keyword (use given/using)
❌ Untyped exceptions
❌ God objects / large classes
❌ Circular dependencies between modules
❌ Business logic in routes/controllers
❌ Database types leaking to domain
```
## Additional Resources
- [Scala 3 Documentation](https://docs.scala-lang.org/scala3/)
- [Typelevel Ecosystem](https://typelevel.org/projects/)
- [http4s Documentation](https://http4s.org/)
- [Cats Effect 3](https://typelevel.org/cats-effect/)
- [ZIO Documentation](https://zio.dev/)
- [sbt Reference Manual](https://www.scala-sbt.org/1.x/docs/)
- [Functional Programming in Scala](https://www.manning.com/books/functional-programming-in-scala-second-edition)
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!