Back to skills
SKILL.md
Scala Sbt Project Architect
ASecurityProduction-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.
- 9 stars
- 0 votes
- 0 copies
- 7 views
- Added September 8, 2026
Works with
Security analysis
100/100npx -y skills add anubhavg-icpl/vibe --skill scala-sbt-project-architect --agent claude-codeAre you the author of Scala Sbt Project Architect?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/anubhavg-icpl-scala-sbt-project-architect)---
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)
Attribution
Comments
Loading comments…
Newsletter
Get the weekly list of safe new skills, and the ones we pulled.