Skip to main content

TypeOps evolved: Scaling type-safe infrastructure to microservices

Picture of Łukasz Biały, Scala Dev Advocate

Łukasz Biały

Scala Dev Advocate
Nov 18, 2025|21 min read
connected_squares_Typeops
sbt_build

1/* endpoint definitions module */
2lazy val `products-endpoints` = project
3 .in(file("products-endpoints"))
4 .settings(
5 scalaVersion := "3.3.6",
6 libraryDependencies ++= Seq(
7 "com.softwaremill.sttp.tapir" %% "tapir-core" % "1.11.41",
8 "com.softwaremill.sttp.tapir" %% "tapir-json-circe" % "1.11.41"
9 )
10 )
11
12/* product service module */
13lazy val `product-service` = project
14 .in(file("product-service"))
15 .dependsOn(`products-endpoints`)
16 .settings(
17 scalaVersion := "3.3.6",
18 libraryDependencies ++= Seq(
19 "com.softwaremill.sttp.tapir" %% "tapir-netty-server-sync" % "1.11.41",
20 "ch.qos.logback" % "logback-classic" % "1.4.11"
21 )
22 )
23
24/* recipes service module */
25lazy val `recipes-service` = project
26 .in(file("recipes-service"))
27 .dependsOn(`products-endpoints`)
28 .settings(
29 scalaVersion := "3.3.6",
30 libraryDependencies ++= Seq(
31 "com.softwaremill.sttp.tapir" %% "tapir-netty-server-sync" % "1.11.41",
32 "com.softwaremill.sttp.tapir" %% "tapir-sttp-client4" % "1.11.41",
33 "com.softwaremill.sttp.client4" %% "core" % "4.0.9",
34 "ch.qos.logback" % "logback-classic" % "1.4.11"
35 )
36 )
1addSbtPlugin("org.virtuslab" % "sbt-yaga-k8s-service" % "0.1.0-SNAPSHOT")
1lazy val `products-endpoints` = project
2 .in(file("products-endpoints"))
3 .yagaK8sServiceOpenApiEndpoints
4 .settings(
5 scalaVersion := "3.3.6",
6 libraryDependencies ++= Seq(
7 "com.softwaremill.sttp.tapir" %% "tapir-json-circe" % "1.11.41"
8 )
9 )
1object ProductsEndpoints derives ExtractEndpoints:
2
3 val getProductEndpoint: PublicEndpoint[Long, Unit, Product, Any] =
4 endpoint.get
5 .in("products" / path[Long]("productId"))
6 .out(jsonBody[Product])
7 .errorOut(statusCode(StatusCode.NotFound))
8
9 // ...
1lazy val `product-service` = project
2 .in(file("product-service"))
3 .yagaOpenApiK8sService(ServerType.NettySync)
4 .dependsOn(`products-endpoints`)
5 .settings(
6 scalaVersion := "3.3.6",
7 libraryDependencies ++= Seq(
8 "ch.qos.logback" % "logback-classic" % "1.4.11"
9 ),
10 dockerBaseImage := "eclipse-temurin:21"
11 )
12
13lazy val `recipes-service` = project
14 .in(file("recipes-service"))
15 .yagaOpenApiK8sService(ServerType.NettySync)
16 .yagaOpenApiClient
17 .dependsOn(`products-endpoints`)
18 .settings(
19 scalaVersion := "3.3.6",
20 libraryDependencies ++= Seq(
21 "ch.qos.logback" % "logback-classic" % "1.4.11"
22 ),
23 dockerBaseImage := "eclipse-temurin:21"
24 )
1case class ServerConfig(
2 someConfigValue: String,
3 productService: OpenApiServiceReference[ProductsEndpoints.type]
4) derives JsonReader
5
6object RecipesService extends NettySyncServerApp[ServerConfig]:
7
8 def serviceName: String = "recipes-service"
9 def serviceVersion: String = "0.1.0-SNAPSHOT"
10
11 override def serverEndpoints(config: ServerConfig): List[ServerEndpoint] =
12 // ...
1// sttp backend, you can use whichever you like!
2lazy val backend: SyncBackend = DefaultSyncBackend()
3given SttpClientInterpreter = SttpClientInterpreter()
4
5// the interpreter is used to turn Endpoints into sttp request templates
6// in this case we want the variant that turns any http errors to exceptions
7lazy val productService = config.productService.toRequestThrowErrors
8
9val productIds: List[Long] = ???
10val request = productService.getNutritionInfosEndpoint(productIds)
11val productNutritionList = request.send(backend).body
1val productImage = ProductService.imageResource(
2 resourceName = "product-image",
3 imageCoordinates = ImageCoordinates(
4 registry = registryName,
5 name = productRepository.name,
6 tag = "0.1.0-SNAPSHOT"
7 ),
8 registry = docker.inputs.RegistryArgs(
9 username = creds.userName,
10 password = creds.password
11 )
12)
sbt_build_yaga

Subscribe to our newsletter and never miss an article