Scala Ecosystem and Popular Libraries: Building Real-World Applications

Introduction

The Scala ecosystem is rich with high-quality libraries and frameworks that enable you to build everything from web services to big data processing systems. Understanding the ecosystem and knowing which libraries to use for different use cases is crucial for productive Scala development.

This lesson provides a comprehensive tour of the most important Scala libraries and frameworks, showing you how to integrate them effectively and make informed decisions about which tools to use for your projects.

Core Infrastructure Libraries

SBT (Scala Build Tool)

// build.sbt - Essential SBT configuration
ThisBuild / version := "0.1.0-SNAPSHOT"
ThisBuild / scalaVersion := "2.13.10"

lazy val root = (project in file("."))
  .settings(
    name := "my-scala-app",

    // Compiler options
    scalacOptions ++= Seq(
      "-deprecation",
      "-encoding", "UTF-8",
      "-feature",
      "-unchecked",
      "-Xlint",
      "-Ywarn-dead-code",
      "-Ywarn-numeric-widen",
      "-Ywarn-value-discard"
    ),

    // Dependencies
    libraryDependencies ++= Seq(
      // Testing
      "org.scalatest" %% "scalatest" % "3.2.15" % Test,
      "org.scalatestplus" %% "scalacheck-1-17" % "3.2.15.0" % Test,

      // JSON handling
      "io.circe" %% "circe-core" % "0.14.5",
      "io.circe" %% "circe-generic" % "0.14.5",
      "io.circe" %% "circe-parser" % "0.14.5",

      // HTTP client
      "com.softwaremill.sttp.client3" %% "core" % "3.8.13",
      "com.softwaremill.sttp.client3" %% "circe" % "3.8.13",

      // Logging
      "ch.qos.logback" % "logback-classic" % "1.4.6",
      "com.typesafe.scala-logging" %% "scala-logging" % "3.9.5",

      // Configuration
      "com.typesafe" % "config" % "1.4.2",

      // Cats for functional programming
      "org.typelevel" %% "cats-core" % "2.9.0",
      "org.typelevel" %% "cats-effect" % "3.4.8"
    ),

    // Test configuration
    Test / parallelExecution := false,
    Test / testOptions += Tests.Argument(TestFrameworks.ScalaTest, "-oD"),

    // Assembly plugin for fat JARs
    assembly / assemblyMergeStrategy := {
      case PathList("META-INF", xs @ _*) => MergeStrategy.discard
      case x => MergeStrategy.first
    }
  )

// project/plugins.sbt
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.1.1")
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.0")
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.10.4")
addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.9.13")

// Example application structure
object BuildUtils {
  // Custom SBT tasks
  val generateBuildInfo = taskKey[Unit]("Generate build information")

  val generateBuildInfoTask = generateBuildInfo := {
    val file = (Compile / sourceManaged).value / "BuildInfo.scala"
    val version = (ThisBuild / version).value
    val scalaVersion = (ThisBuild / scalaVersion).value
    val buildTime = java.time.Instant.now().toString

    val content = s"""
      |object BuildInfo {
      |  val version: String = "$version"
      |  val scalaVersion: String = "$scalaVersion"
      |  val buildTime: String = "$buildTime"
      |}
      |""".stripMargin

    IO.write(file, content)
    println(s"Generated BuildInfo at $file")
  }
}

// Multi-module build example
lazy val common = (project in file("modules/common"))
  .settings(
    name := "common",
    libraryDependencies ++= Seq(
      "org.typelevel" %% "cats-core" % "2.9.0"
    )
  )

lazy val core = (project in file("modules/core"))
  .settings(
    name := "core"
  )
  .dependsOn(common)

lazy val api = (project in file("modules/api"))
  .settings(
    name := "api",
    libraryDependencies ++= Seq(
      "com.typesafe.akka" %% "akka-http" % "10.5.0",
      "com.typesafe.akka" %% "akka-stream" % "2.8.0"
    )
  )
  .dependsOn(core)

lazy val web = (project in file("modules/web"))
  .settings(
    name := "web",
    libraryDependencies ++= Seq(
      "com.typesafe.play" %% "play" % "2.8.19"
    )
  )
  .dependsOn(core)

Configuration Management

// Using Typesafe Config
import com.typesafe.config.{Config, ConfigFactory}

// application.conf
"""
app {
  name = "MyScalaApp"
  version = "1.0.0"

  database {
    url = "jdbc:postgresql://localhost:5432/mydb"
    url = ${?DATABASE_URL}  # Override with environment variable
    username = "user"
    username = ${?DB_USERNAME}
    password = "password"
    password = ${?DB_PASSWORD}
    pool-size = 10
  }

  server {
    host = "0.0.0.0"
    port = 8080
    port = ${?PORT}
  }

  features {
    enable-metrics = true
    enable-caching = false
    max-connections = 100
  }
}
"""

case class DatabaseConfig(
  url: String,
  username: String,
  password: String,
  poolSize: Int
)

case class ServerConfig(
  host: String,
  port: Int
)

case class FeaturesConfig(
  enableMetrics: Boolean,
  enableCaching: Boolean,
  maxConnections: Int
)

case class AppConfig(
  name: String,
  version: String,
  database: DatabaseConfig,
  server: ServerConfig,
  features: FeaturesConfig
)

object ConfigLoader {
  def load(): AppConfig = {
    val config: Config = ConfigFactory.load()

    val appConfig = config.getConfig("app")
    val dbConfig = appConfig.getConfig("database")
    val serverConfig = appConfig.getConfig("server")
    val featuresConfig = appConfig.getConfig("features")

    AppConfig(
      name = appConfig.getString("name"),
      version = appConfig.getString("version"),
      database = DatabaseConfig(
        url = dbConfig.getString("url"),
        username = dbConfig.getString("username"),
        password = dbConfig.getString("password"),
        poolSize = dbConfig.getInt("pool-size")
      ),
      server = ServerConfig(
        host = serverConfig.getString("host"),
        port = serverConfig.getInt("port")
      ),
      features = FeaturesConfig(
        enableMetrics = featuresConfig.getBoolean("enable-metrics"),
        enableCaching = featuresConfig.getBoolean("enable-caching"),
        maxConnections = featuresConfig.getInt("max-connections")
      )
    )
  }

  // Configuration with validation
  def loadWithValidation(): Either[String, AppConfig] = {
    try {
      val config = load()

      // Validate configuration
      val errors = scala.collection.mutable.ListBuffer[String]()

      if (config.name.isEmpty) errors += "App name cannot be empty"
      if (config.database.url.isEmpty) errors += "Database URL cannot be empty"
      if (config.server.port < 1 || config.server.port > 65535) errors += "Invalid port number"
      if (config.features.maxConnections <= 0) errors += "Max connections must be positive"

      if (errors.nonEmpty) {
        Left(s"Configuration errors: ${errors.mkString(", ")}")
      } else {
        Right(config)
      }
    } catch {
      case e: Exception => Left(s"Failed to load configuration: ${e.getMessage}")
    }
  }
}

// Usage
val config = ConfigLoader.loadWithValidation() match {
  case Right(cfg) => 
    println(s"Configuration loaded successfully: ${cfg.name} v${cfg.version}")
    println(s"Server will run on ${cfg.server.host}:${cfg.server.port}")
    cfg
  case Left(error) => 
    println(s"Configuration error: $error")
    sys.exit(1)
}

// Environment-specific configurations
object Environment extends Enumeration {
  val Development, Staging, Production = Value

  def current: Environment.Value = {
    sys.env.get("ENVIRONMENT").map(_.toLowerCase) match {
      case Some("development") => Development
      case Some("staging") => Staging
      case Some("production") => Production
      case _ => Development
    }
  }
}

def loadEnvironmentConfig(): AppConfig = {
  val baseConfig = ConfigFactory.load()
  val envConfig = Environment.current match {
    case Environment.Development => ConfigFactory.load("development.conf")
    case Environment.Staging => ConfigFactory.load("staging.conf")
    case Environment.Production => ConfigFactory.load("production.conf")
  }

  // Layer environment-specific config over base config
  val config = envConfig.withFallback(baseConfig)
  println(s"Loaded configuration for ${Environment.current} environment")

  // Extract configuration using the layered config
  ConfigLoader.load()  // Would use the layered config in real implementation
}

// Configuration with type safety using PureConfig
import pureconfig._
import pureconfig.generic.auto._

case class TypeSafeConfig(
  database: DatabaseConfig,
  server: ServerConfig,
  features: FeaturesConfig
)

def loadTypeSafeConfig(): Either[ConfigReaderFailures, TypeSafeConfig] = {
  ConfigSource.default.at("app").load[TypeSafeConfig]
}

// Usage with better error handling
loadTypeSafeConfig() match {
  case Right(cfg) => println(s"Type-safe config loaded: $cfg")
  case Left(failures) => 
    println("Configuration failures:")
    failures.toList.foreach(failure => println(s"  - ${failure.description}"))
}

Functional Programming Libraries

Cats - Functional Programming Abstractions

import cats._
import cats.data._
import cats.implicits._

// Basic type class usage
def combineAll[A](list: List[A])(implicit monoid: Monoid[A]): A = {
  list.foldLeft(monoid.empty)(monoid.combine)
}

println(combineAll(List(1, 2, 3, 4, 5)))  // Uses Int Monoid
println(combineAll(List("hello", " ", "world")))  // Uses String Monoid
println(combineAll(List(List(1, 2), List(3, 4), List(5))))  // Uses List Monoid

// Validated for error accumulation
case class PersonValidation(name: String, age: Int, email: String)

type ValidationResult[A] = ValidatedNel[String, A]

def validateName(name: String): ValidationResult[String] = {
  if (name.nonEmpty && name.length >= 2) name.validNel
  else "Name must be at least 2 characters".invalidNel
}

def validateAge(age: Int): ValidationResult[Int] = {
  if (age >= 0 && age <= 150) age.validNel
  else "Age must be between 0 and 150".invalidNel
}

def validateEmail(email: String): ValidationResult[String] = {
  if (email.contains("@") && email.contains(".")) email.validNel
  else "Email must be valid".invalidNel
}

def validatePerson(name: String, age: Int, email: String): ValidationResult[PersonValidation] = {
  (
    validateName(name),
    validateAge(age),
    validateEmail(email)
  ).mapN(PersonValidation)
}

// Test validation
val validPerson = validatePerson("John Doe", 30, "john@example.com")
val invalidPerson = validatePerson("", -5, "invalid-email")

validPerson match {
  case Valid(person) => println(s"✓ Valid person: $person")
  case Invalid(errors) => println(s"✗ Validation errors: ${errors.toList}")
}

invalidPerson match {
  case Valid(person) => println(s"✓ Valid person: $person")
  case Invalid(errors) => 
    println("✗ Validation errors:")
    errors.toList.foreach(error => println(s"  - $error"))
}

// Either with error handling
def divide(a: Double, b: Double): Either[String, Double] = {
  if (b == 0) Left("Division by zero")
  else Right(a / b)
}

def sqrt(x: Double): Either[String, Double] = {
  if (x < 0) Left("Cannot take square root of negative number")
  else Right(math.sqrt(x))
}

// Chain operations with for-comprehension
def complexCalculation(a: Double, b: Double, c: Double): Either[String, Double] = {
  for {
    divided <- divide(a, b)
    added = divided + c
    sqrtResult <- sqrt(added)
  } yield sqrtResult
}

println(complexCalculation(10, 2, 9))  // Right(4.0)
println(complexCalculation(10, 0, 9))  // Left(Division by zero)
println(complexCalculation(10, 2, -20))  // Left(Cannot take square root of negative number)

// StateT for stateful computations
import cats.data.StateT

type GameState = StateT[Id, Int, Int]  // State[Int, Int] representing score

def addPoints(points: Int): GameState = StateT { currentScore =>
  val newScore = currentScore + points
  (newScore, points)
}

def multiplyScore(factor: Int): GameState = StateT { currentScore =>
  val newScore = currentScore * factor
  (newScore, newScore)
}

def bonusRound: GameState = StateT { currentScore =>
  val bonus = if (currentScore > 100) 50 else 10
  val newScore = currentScore + bonus
  (newScore, bonus)
}

// Compose stateful operations
val gameSequence: GameState = for {
  points1 <- addPoints(25)
  points2 <- addPoints(30)
  multiplied <- multiplyScore(2)
  bonus <- bonusRound
} yield bonus

val (finalScore, lastBonus) = gameSequence.run(10)  // Starting with score 10
println(s"Final score: $finalScore, Last bonus: $lastBonus")

// Reader for dependency injection
import cats.data.Reader

case class DatabaseConfig(url: String, maxConnections: Int)
case class ApiConfig(endpoint: String, timeout: Int)
case class AppEnvironment(dbConfig: DatabaseConfig, apiConfig: ApiConfig)

def createConnection: Reader[DatabaseConfig, String] = Reader { config =>
  s"Connected to ${config.url} with ${config.maxConnections} max connections"
}

def callApi: Reader[ApiConfig, String] = Reader { config =>
  s"Called API at ${config.endpoint} with ${config.timeout}ms timeout"
}

def initializeApp: Reader[AppEnvironment, String] = for {
  dbConnection <- createConnection.local[AppEnvironment](_.dbConfig)
  apiResponse <- callApi.local[AppEnvironment](_.apiConfig)
} yield s"App initialized: $dbConnection, $apiResponse"

val environment = AppEnvironment(
  DatabaseConfig("postgresql://localhost:5432/app", 20),
  ApiConfig("https://api.service.com", 5000)
)

val appInitResult = initializeApp.run(environment)
println(appInitResult)

// Writer for logging
import cats.data.Writer

type Logged[A] = Writer[List[String], A]

def factorial(n: Int): Logged[Int] = {
  if (n <= 1) {
    Writer(List(s"Base case: factorial($n) = 1"), 1)
  } else {
    for {
      prev <- factorial(n - 1)
      result = n * prev
      _ <- Writer.tell(List(s"Calculated: factorial($n) = $n * $prev = $result"))
    } yield result
  }
}

val (logs, result) = factorial(5).run
println(s"Result: $result")
println("Computation log:")
logs.foreach(log => println(s"  $log"))

// Kleisli for composing monadic functions
import cats.data.Kleisli
import cats.instances.option._

type OptionKleisli[A, B] = Kleisli[Option, A, B]

def parseIntSafely: OptionKleisli[String, Int] = Kleisli { str =>
  try Some(str.toInt) catch { case _: NumberFormatException => None }
}

def isPositive: OptionKleisli[Int, Int] = Kleisli { n =>
  if (n > 0) Some(n) else None
}

def double: OptionKleisli[Int, Int] = Kleisli { n =>
  Some(n * 2)
}

// Compose the operations
val parseAndProcess: OptionKleisli[String, Int] = 
  parseIntSafely andThen isPositive andThen double

println(parseAndProcess.run("42"))    // Some(84)
println(parseAndProcess.run("-5"))    // None (negative)
println(parseAndProcess.run("abc"))   // None (not a number)

// Custom type class instances
case class Money(amount: BigDecimal, currency: String)

implicit val moneyMonoid: Monoid[Money] = new Monoid[Money] {
  def empty: Money = Money(BigDecimal(0), "USD")

  def combine(x: Money, y: Money): Money = {
    require(x.currency == y.currency, "Cannot combine different currencies")
    Money(x.amount + y.amount, x.currency)
  }
}

implicit val moneyShow: Show[Money] = Show.show { money =>
  f"${money.amount}%.2f ${money.currency}"
}

val purchases = List(
  Money(BigDecimal("19.99"), "USD"),
  Money(BigDecimal("25.50"), "USD"),
  Money(BigDecimal("8.75"), "USD")
)

val total = combineAll(purchases)
println(s"Total: ${total.show}")

// Traverse for working with collections
def parseAllInts(strings: List[String]): Option[List[Int]] = {
  strings.traverse(parseIntSafely.run)
}

val validNumbers = List("1", "2", "3", "4", "5")
val invalidNumbers = List("1", "abc", "3")

println(s"Valid parsing: ${parseAllInts(validNumbers)}")
println(s"Invalid parsing: ${parseAllInts(invalidNumbers)}")

// Parallel processing with Parallel type class
import cats.Parallel
import cats.instances.either._

// Simulate async operations
def fetchUser(id: Int): Either[String, String] = {
  if (id > 0) Right(s"User$id") else Left(s"Invalid user ID: $id")
}

def fetchPosts(userId: String): Either[String, List[String]] = {
  Right(List(s"${userId}_post1", s"${userId}_post2"))
}

def fetchProfile(userId: String): Either[String, String] = {
  Right(s"${userId}_profile")
}

// Sequential vs parallel execution
def fetchUserDataSequential(id: Int): Either[String, (String, List[String], String)] = {
  for {
    user <- fetchUser(id)
    posts <- fetchPosts(user)
    profile <- fetchProfile(user)
  } yield (user, posts, profile)
}

// This would run in parallel if using async types like IO
def fetchUserDataParallel(id: Int): Either[String, (String, List[String], String)] = {
  (fetchUser(id), fetchUser(id).flatMap(fetchPosts), fetchUser(id).flatMap(fetchProfile)).parMapN {
    case (user, posts, profile) => (user, posts, profile)
  }
}

println(s"Sequential fetch: ${fetchUserDataSequential(123)}")
println(s"Parallel fetch: ${fetchUserDataParallel(123)}")

Cats Effect - Functional Effects and Concurrency

import cats.effect._
import cats.effect.unsafe.implicits.global
import scala.concurrent.duration._

// Basic IO operations
val helloWorld: IO[Unit] = IO.println("Hello, World!")
val greeting: IO[String] = IO.pure("Hello")
val readLine: IO[String] = IO.readLine

// Running IO programs
helloWorld.unsafeRunSync()

val result = greeting.unsafeRunSync()
println(s"Greeting result: $result")

// Error handling in IO
def divide(a: Int, b: Int): IO[Double] = {
  if (b == 0) IO.raiseError(new ArithmeticException("Division by zero"))
  else IO.pure(a.toDouble / b)
}

val safeComputation = divide(10, 2).handleErrorWith { error =>
  IO.println(s"Error occurred: ${error.getMessage}") *> IO.pure(0.0)
}

val errorComputation = divide(10, 0).handleErrorWith { error =>
  IO.println(s"Error occurred: ${error.getMessage}") *> IO.pure(Double.NaN)
}

println(s"Safe result: ${safeComputation.unsafeRunSync()}")
println(s"Error result: ${errorComputation.unsafeRunSync()}")

// Resource management
def createFileResource(path: String): Resource[IO, java.io.PrintWriter] = {
  Resource.make(
    IO.delay(new java.io.PrintWriter(path))  // Acquire
  )(writer => 
    IO.delay(writer.close()) *> IO.println(s"Closed file: $path")  // Release
  )
}

val writeToFile: IO[Unit] = createFileResource("output.txt").use { writer =>
  IO.delay {
    writer.println("Hello from Cats Effect!")
    writer.println("Resource management is automatic")
  }
}

// This would create and automatically close the file
// writeToFile.unsafeRunSync()

// Concurrent operations
def simulateWork(name: String, duration: Duration): IO[String] = {
  IO.sleep(duration) *> IO.pure(s"$name completed")
}

val concurrentWork: IO[List[String]] = List(
  simulateWork("Task 1", 100.millis),
  simulateWork("Task 2", 200.millis),
  simulateWork("Task 3", 150.millis)
).parSequence

// Fiber-based concurrency
val fiberExample: IO[Unit] = for {
  fiber1 <- simulateWork("Background task 1", 500.millis).start
  fiber2 <- simulateWork("Background task 2", 300.millis).start
  _ <- IO.println("Started background tasks")
  result1 <- fiber1.join
  result2 <- fiber2.join
  _ <- IO.println(s"Results: ${result1.getOrElse("Failed")}, ${result2.getOrElse("Failed")}")
} yield ()

println("Running fiber example...")
fiberExample.unsafeRunSync()

// Producer-Consumer with concurrent queues
import cats.effect.std.Queue

def producer(queue: Queue[IO, String], items: List[String]): IO[Unit] = {
  items.traverse_(item => 
    IO.println(s"Producing: $item") *> queue.offer(item) *> IO.sleep(100.millis)
  )
}

def consumer(queue: Queue[IO, String], name: String): IO[Unit] = {
  def consumeLoop: IO[Unit] = for {
    item <- queue.take
    _ <- IO.println(s"$name consumed: $item")
    _ <- IO.sleep(150.millis)
    _ <- consumeLoop
  } yield ()

  consumeLoop
}

val producerConsumerExample: IO[Unit] = for {
  queue <- Queue.bounded[IO, String](10)
  items = List("item1", "item2", "item3", "item4", "item5")

  // Start producer and consumer concurrently
  producerFiber <- producer(queue, items).start
  consumer1Fiber <- consumer(queue, "Consumer1").start
  consumer2Fiber <- consumer(queue, "Consumer2").start

  // Wait for producer to finish
  _ <- producerFiber.join

  // Give consumers time to process
  _ <- IO.sleep(1.second)

  // Cancel consumers
  _ <- consumer1Fiber.cancel
  _ <- consumer2Fiber.cancel

  _ <- IO.println("Producer-consumer example completed")
} yield ()

// Uncomment to run:
// producerConsumerExample.unsafeRunSync()

// Ref for safe concurrent state
import cats.effect.Ref

def counterExample: IO[Unit] = for {
  counter <- Ref.of[IO, Int](0)

  // Multiple fibers incrementing counter
  fibers <- (1 to 10).toList.traverse { i =>
    (for {
      _ <- IO.sleep((scala.util.Random.nextInt(100)).millis)
      current <- counter.updateAndGet(_ + 1)
      _ <- IO.println(s"Fiber $i: counter = $current")
    } yield ()).start
  }

  // Wait for all fibers to complete
  _ <- fibers.traverse_(_.join)

  finalValue <- counter.get
  _ <- IO.println(s"Final counter value: $finalValue")
} yield ()

println("Running counter example...")
counterExample.unsafeRunSync()

// Deferred for coordination
import cats.effect.Deferred

def coordinationExample: IO[Unit] = for {
  signal <- Deferred[IO, String]

  // Worker that waits for signal
  worker <- (for {
    _ <- IO.println("Worker waiting for signal...")
    message <- signal.get
    _ <- IO.println(s"Worker received: $message")
  } yield ()).start

  // Coordinator that sends signal after delay
  coordinator <- (for {
    _ <- IO.sleep(2.seconds)
    _ <- IO.println("Coordinator sending signal...")
    _ <- signal.complete("Start working!")
  } yield ()).start

  // Wait for both to complete
  _ <- worker.join
  _ <- coordinator.join
} yield ()

println("Running coordination example...")
coordinationExample.unsafeRunSync()

// Async boundary and thread pools
import cats.effect.std.Console

def cpuIntensiveTask(n: Int): IO[Long] = IO.blocking {
  // Simulate CPU-intensive work
  (1L to n).sum
}

def ioTask(message: String): IO[Unit] = IO.blocking {
  // Simulate I/O work
  Thread.sleep(100)
  println(s"I/O Task: $message")
}

val mixedWorkload: IO[Unit] = for {
  _ <- IO.println("Starting mixed workload...")

  // CPU tasks on compute pool
  cpuResults <- List(1000000, 2000000, 3000000).traverse(cpuIntensiveTask)

  // I/O tasks on blocking pool
  _ <- List("File 1", "File 2", "File 3").parTraverse(ioTask)

  _ <- IO.println(s"CPU results: ${cpuResults.sum}")
  _ <- IO.println("Mixed workload completed")
} yield ()

println("Running mixed workload example...")
mixedWorkload.unsafeRunSync()

// Stream processing with fs2 integration
import fs2.Stream

def streamExample: IO[Unit] = {
  val numbers: Stream[IO, Int] = Stream.range(1, 11)

  val processed: Stream[IO, String] = numbers
    .evalMap(n => IO.delay(n * n))  // Square each number
    .filter(_ % 2 == 0)             // Keep only even squares
    .map(n => s"Even square: $n")   // Format as string
    .evalTap(s => IO.println(s))    // Print each result

  processed.compile.drain  // Run the stream
}

println("Running stream example...")
streamExample.unsafeRunSync()

Web Development Frameworks

Akka HTTP - Reactive Web Services

import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.model._
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route
import akka.stream.ActorMaterializer
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success}

// Basic Akka HTTP server
object SimpleAkkaHttpServer {

  implicit val system: ActorSystem = ActorSystem("simple-server")
  implicit val materializer: ActorMaterializer = ActorMaterializer()
  implicit val executionContext: ExecutionContext = system.dispatcher

  // Data models
  case class User(id: Int, name: String, email: String)
  case class CreateUserRequest(name: String, email: String)
  case class ApiResponse[T](success: Boolean, data: Option[T] = None, message: Option[String] = None)

  // In-memory user store (in real app, use database)
  var users: Map[Int, User] = Map(
    1 -> User(1, "Alice", "alice@example.com"),
    2 -> User(2, "Bob", "bob@example.com")
  )
  var nextId = 3

  // JSON support with spray-json
  import spray.json._
  import spray.json.DefaultJsonProtocol._
  import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._

  implicit val userFormat: RootJsonFormat[User] = jsonFormat3(User)
  implicit val createUserFormat: RootJsonFormat[CreateUserRequest] = jsonFormat2(CreateUserRequest)
  implicit val apiResponseFormat: RootJsonFormat[ApiResponse[User]] = jsonFormat3(ApiResponse[User])
  implicit val apiResponseUsersFormat: RootJsonFormat[ApiResponse[List[User]]] = jsonFormat3(ApiResponse[List[User]])

  // Routes
  def userRoutes: Route = {
    pathPrefix("api" / "users") {
      concat(
        // GET /api/users - list all users
        pathEnd {
          get {
            complete(ApiResponse(success = true, data = Some(users.values.toList)))
          }
        },

        // GET /api/users/{id} - get specific user
        path(IntNumber) { userId =>
          get {
            users.get(userId) match {
              case Some(user) => complete(ApiResponse(success = true, data = Some(user)))
              case None => complete(StatusCodes.NotFound, ApiResponse[User](success = false, message = Some("User not found")))
            }
          }
        },

        // POST /api/users - create new user
        pathEnd {
          post {
            entity(as[CreateUserRequest]) { request =>
              val newUser = User(nextId, request.name, request.email)
              users += nextId -> newUser
              nextId += 1
              complete(StatusCodes.Created, ApiResponse(success = true, data = Some(newUser)))
            }
          }
        },

        // PUT /api/users/{id} - update user
        path(IntNumber) { userId =>
          put {
            entity(as[CreateUserRequest]) { request =>
              users.get(userId) match {
                case Some(_) =>
                  val updatedUser = User(userId, request.name, request.email)
                  users += userId -> updatedUser
                  complete(ApiResponse(success = true, data = Some(updatedUser)))
                case None =>
                  complete(StatusCodes.NotFound, ApiResponse[User](success = false, message = Some("User not found")))
              }
            }
          }
        },

        // DELETE /api/users/{id} - delete user
        path(IntNumber) { userId =>
          delete {
            users.get(userId) match {
              case Some(user) =>
                users -= userId
                complete(ApiResponse(success = true, data = Some(user), message = Some("User deleted")))
              case None =>
                complete(StatusCodes.NotFound, ApiResponse[User](success = false, message = Some("User not found")))
            }
          }
        }
      )
    }
  }

  // Health check route
  def healthRoute: Route = {
    path("health") {
      get {
        complete(Map("status" -> "ok", "timestamp" -> System.currentTimeMillis()))
      }
    }
  }

  // Static content route
  def staticRoute: Route = {
    pathPrefix("static") {
      getFromResourceDirectory("static")
    }
  }

  // Main routes
  val routes: Route = concat(
    userRoutes,
    healthRoute,
    staticRoute,
    pathSingleSlash {
      complete(HttpResponse(StatusCodes.OK, entity = "Welcome to Akka HTTP API"))
    }
  )

  def startServer(host: String = "localhost", port: Int = 8080): Unit = {
    val bindingFuture = Http().bindAndHandle(routes, host, port)

    bindingFuture.onComplete {
      case Success(binding) =>
        println(s"Server running at http://$host:$port/")
        println(s"Server bound to ${binding.localAddress}")
      case Failure(exception) =>
        println(s"Failed to bind to $host:$port: ${exception.getMessage}")
        system.terminate()
    }
  }
}

// Advanced Akka HTTP features
object AdvancedAkkaHttpServer {

  import akka.http.scaladsl.server.directives.Credentials
  import akka.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials}
  import scala.concurrent.duration._

  // Authentication
  def authenticateBasic(credentials: Credentials): Option[String] = {
    credentials match {
      case p @ Credentials.Provided(username) if p.verify("secret") => Some(username)
      case _ => None
    }
  }

  // Middleware for request logging
  import akka.http.scaladsl.server.directives.LoggingMagnet
  import akka.event.Logging

  def requestLogger = LoggingMagnet(_ => {
    case r @ HttpRequest(method, uri, _, _, _) =>
      println(s"${method.value} ${uri}")
      r
  })

  // CORS support
  import akka.http.scaladsl.model.headers._
  import akka.http.scaladsl.model.HttpMethods._

  def corsHandler: Directive0 = {
    respondWithHeaders(
      `Access-Control-Allow-Origin`.*,
      `Access-Control-Allow-Methods`(GET, POST, PUT, DELETE, OPTIONS),
      `Access-Control-Allow-Headers`("Authorization", "Content-Type")
    )
  }

  // Rate limiting (simplified)
  import scala.collection.mutable

  val requestCounts: mutable.Map[String, (Long, Int)] = mutable.Map()

  def rateLimit(maxRequests: Int, windowMs: Long): Directive0 = {
    extractClientIP { ip =>
      val clientIp = ip.toString
      val now = System.currentTimeMillis()

      requestCounts.get(clientIp) match {
        case Some((windowStart, count)) if now - windowStart < windowMs =>
          if (count >= maxRequests) {
            complete(StatusCodes.TooManyRequests, "Rate limit exceeded")
          } else {
            requestCounts(clientIp) = (windowStart, count + 1)
            pass
          }
        case _ =>
          requestCounts(clientIp) = (now, 1)
          pass
      }
    }
  }

  // Streaming response
  def streamingRoute: Route = {
    path("stream") {
      get {
        import akka.stream.scaladsl.Source
        import akka.util.ByteString

        val source = Source(1 to 100)
          .map(i => s"data: Item $i\n")
          .map(ByteString.apply)
          .throttle(1, 100.millis)

        complete(HttpResponse(
          entity = HttpEntity.Chunked.fromData(ContentTypes.`text/plain(UTF-8)`, source)
        ))
      }
    }
  }

  // WebSocket support
  import akka.http.scaladsl.model.ws.{Message, TextMessage}
  import akka.stream.scaladsl.{Flow, Sink, Source}

  def websocketRoute: Route = {
    path("ws") {
      get {
        val greeterWebSocketService = Flow[Message]
          .mapConcat {
            case tm: TextMessage =>
              TextMessage(Source.single(s"Echo: ${tm.textStream.runFold("")(_ + _)}")) :: Nil
            case _ =>
              Nil
          }

        handleWebSocketMessages(greeterWebSocketService)
      }
    }
  }

  // Complete advanced routes
  val advancedRoutes: Route = {
    logRequestResult(requestLogger) {
      corsHandler {
        rateLimit(100, 60000) {  // 100 requests per minute
          concat(
            pathPrefix("api") {
              authenticateBasic("realm", authenticateBasic) { username =>
                pathPrefix("secure") {
                  complete(s"Hello, $username! This is a secure endpoint.")
                }
              }
            },
            streamingRoute,
            websocketRoute
          )
        }
      }
    }
  }
}

// Testing Akka HTTP routes
import akka.http.scaladsl.testkit.ScalatestRouteTest
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec

class UserRoutesSpec extends AnyWordSpec with Matchers with ScalatestRouteTest {

  "User routes" should {
    "return all users on GET /api/users" in {
      Get("/api/users") ~> SimpleAkkaHttpServer.userRoutes ~> check {
        status shouldEqual StatusCodes.OK
        // Additional response checking would go here
      }
    }

    "create a new user on POST /api/users" in {
      val newUser = SimpleAkkaHttpServer.CreateUserRequest("Charlie", "charlie@example.com")

      Post("/api/users", newUser) ~> SimpleAkkaHttpServer.userRoutes ~> check {
        status shouldEqual StatusCodes.Created
        // Check response body
      }
    }

    "return 404 for non-existent user" in {
      Get("/api/users/999") ~> SimpleAkkaHttpServer.userRoutes ~> check {
        status shouldEqual StatusCodes.NotFound
      }
    }
  }
}

// Usage example
object AkkaHttpApp extends App {
  // This would start the server in a real application
  // SimpleAkkaHttpServer.startServer("localhost", 8080)

  // For demonstration, just show that the routes are defined
  println("Akka HTTP server routes defined successfully")
  println("To run: call SimpleAkkaHttpServer.startServer()")
}

Play Framework - Full-Stack Web Applications

// Play Framework application structure

// app/controllers/HomeController.scala
import javax.inject._
import play.api.mvc._
import play.api.libs.json._
import scala.concurrent.{ExecutionContext, Future}

@Singleton
class HomeController @Inject()(val controllerComponents: ControllerComponents)
                              (implicit ec: ExecutionContext) extends BaseController {

  def index() = Action { implicit request: Request[AnyContent] =>
    Ok(views.html.index())
  }

  def hello(name: String) = Action {
    Ok(s"Hello, $name!")
  }

  def api() = Action {
    Ok(Json.obj(
      "message" -> "Welcome to the API",
      "timestamp" -> System.currentTimeMillis(),
      "version" -> "1.0.0"
    ))
  }
}

// app/controllers/UserController.scala
import models.{User, UserRepository}
import javax.inject._

@Singleton
class UserController @Inject()(
  val controllerComponents: ControllerComponents,
  userRepository: UserRepository
)(implicit ec: ExecutionContext) extends BaseController {

  implicit val userWrites: Writes[User] = Json.writes[User]
  implicit val userReads: Reads[User] = Json.reads[User]

  def list() = Action.async { implicit request =>
    userRepository.findAll().map { users =>
      Ok(Json.toJson(users))
    }
  }

  def show(id: Long) = Action.async { implicit request =>
    userRepository.findById(id).map {
      case Some(user) => Ok(Json.toJson(user))
      case None => NotFound(Json.obj("error" -> "User not found"))
    }
  }

  def create() = Action.async(parse.json) { implicit request =>
    request.body.validate[User].fold(
      errors => Future.successful(BadRequest(Json.obj("errors" -> JsError.toJson(errors)))),
      user => {
        userRepository.create(user).map { createdUser =>
          Created(Json.toJson(createdUser))
        }
      }
    )
  }

  def update(id: Long) = Action.async(parse.json) { implicit request =>
    request.body.validate[User].fold(
      errors => Future.successful(BadRequest(Json.obj("errors" -> JsError.toJson(errors)))),
      user => {
        userRepository.update(id, user).map {
          case Some(updatedUser) => Ok(Json.toJson(updatedUser))
          case None => NotFound(Json.obj("error" -> "User not found"))
        }
      }
    )
  }

  def delete(id: Long) = Action.async { implicit request =>
    userRepository.delete(id).map { deleted =>
      if (deleted) Ok(Json.obj("message" -> "User deleted"))
      else NotFound(Json.obj("error" -> "User not found"))
    }
  }
}

// app/models/User.scala
case class User(
  id: Option[Long] = None,
  name: String,
  email: String,
  createdAt: Option[java.time.Instant] = None
)

// app/models/UserRepository.scala
import javax.inject.{Inject, Singleton}
import play.api.db.slick.DatabaseConfigProvider
import slick.jdbc.JdbcProfile
import scala.concurrent.{ExecutionContext, Future}

@Singleton
class UserRepository @Inject()(dbConfigProvider: DatabaseConfigProvider)
                              (implicit ec: ExecutionContext) {

  private val dbConfig = dbConfigProvider.get[JdbcProfile]

  import dbConfig._
  import profile.api._

  class UserTable(tag: Tag) extends Table[User](tag, "users") {
    def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
    def name = column[String]("name")
    def email = column[String]("email")
    def createdAt = column[java.time.Instant]("created_at")

    def * = (id.?, name, email, createdAt.?) <> ((User.apply _).tupled, User.unapply)
  }

  private val users = TableQuery[UserTable]

  def findAll(): Future[Seq[User]] = db.run(users.result)

  def findById(id: Long): Future[Option[User]] = 
    db.run(users.filter(_.id === id).result.headOption)

  def create(user: User): Future[User] = {
    val userWithTimestamp = user.copy(createdAt = Some(java.time.Instant.now()))
    db.run(
      (users returning users.map(_.id) into ((user, id) => user.copy(id = Some(id))))
        += userWithTimestamp
    )
  }

  def update(id: Long, user: User): Future[Option[User]] = {
    val updateAction = users.filter(_.id === id).update(user.copy(id = Some(id)))
    db.run(updateAction).flatMap { rowsUpdated =>
      if (rowsUpdated > 0) findById(id)
      else Future.successful(None)
    }
  }

  def delete(id: Long): Future[Boolean] = {
    db.run(users.filter(_.id === id).delete).map(_ > 0)
  }
}

// conf/routes
"""
# Routes
# This file defines all application routes (Higher priority routes first)

# Home page
GET     /                           controllers.HomeController.index
GET     /hello/:name                controllers.HomeController.hello(name: String)

# API endpoints
GET     /api                        controllers.HomeController.api

# User API
GET     /api/users                  controllers.UserController.list
GET     /api/users/:id              controllers.UserController.show(id: Long)
POST    /api/users                  controllers.UserController.create
PUT     /api/users/:id              controllers.UserController.update(id: Long)
DELETE  /api/users/:id              controllers.UserController.delete(id: Long)

# Map static resources from the /public folder to the /assets URL path
GET     /assets/*file               controllers.Assets.versioned(path="/public", file: Asset)
"""

// conf/application.conf
"""
# Database configuration
slick.dbs.default.profile="slick.jdbc.H2Profile$"
slick.dbs.default.db.driver="org.h2.Driver"
slick.dbs.default.db.url="jdbc:h2:mem:play;MODE=MYSQL"

# Play configuration
play.http.secret.key = "changeme"
play.i18n.langs = [ "en" ]

# Application configuration
app.name = "Play Scala Application"
app.version = "1.0.0"
"""

// app/views/index.scala.html (Twirl template)
"""
@()

@main("Welcome to Play") {
    <h1>Welcome to Play!</h1>
    <p>This is a Scala Play Framework application.</p>

    <h2>API Endpoints</h2>
    <ul>
        <li><a href="/api">API Info</a></li>
        <li><a href="/api/users">List Users</a></li>
        <li><a href="/hello/World">Hello World</a></li>
    </ul>
}
"""

// app/views/main.scala.html
"""
@(title: String)(content: Html)

<!DOCTYPE html>
<html lang="en">
    <head>
        @* Here's where we render the page title `String`. *@
        <title>@title</title>
        <link rel="stylesheet" media="screen" href="@routes.Assets.versioned("stylesheets/main.css")">
        <link rel="shortcut icon" type="image/png" href="@routes.Assets.versioned("images/favicon.png")">
    </head>
    <body>
        @* And here's where we render the `Html` object containing
         * the page content. *@
        @content

        <script src="@routes.Assets.versioned("javascripts/main.js")" type="text/javascript"></script>
    </body>
</html>
"""

// Module for dependency injection (app/modules/AppModule.scala)
import com.google.inject.AbstractModule
import play.api.{Configuration, Environment}

class AppModule(environment: Environment, configuration: Configuration) extends AbstractModule {
  override def configure(): Unit = {
    // Bind services and repositories
    bind(classOf[UserRepository]).asEagerSingleton()

    // Custom configuration bindings
    bind(classOf[String]).annotatedWith(Names.named("app.name"))
      .toInstance(configuration.get[String]("app.name"))
  }
}

// Action composition for authentication (app/actions/AuthAction.scala)
import play.api.mvc._
import scala.concurrent.{ExecutionContext, Future}

case class UserRequest[A](userId: Long, request: Request[A]) extends WrappedRequest[A](request)

class AuthAction @Inject()(val parser: BodyParsers.Default)
                          (implicit val executionContext: ExecutionContext)
  extends ActionBuilder[UserRequest, AnyContent] with ActionTransformer[Request, UserRequest] {

  def transform[A](request: Request[A]) = Future.successful {
    // In real application, extract user from session/token
    val userId = request.session.get("userId").map(_.toLong).getOrElse(1L)
    UserRequest(userId, request)
  }
}

// Usage in controller with authentication
class SecureController @Inject()(
  val controllerComponents: ControllerComponents,
  authAction: AuthAction
) extends BaseController {

  def profile() = authAction { implicit request: UserRequest[AnyContent] =>
    Ok(Json.obj(
      "userId" -> request.userId,
      "message" -> "This is your profile"
    ))
  }
}

// Testing Play applications
import org.scalatest._
import org.scalatestplus.play._
import play.api.test._
import play.api.test.Helpers._

class HomeControllerSpec extends PlaySpec with GuiceOneAppPerTest with Injecting {

  "HomeController GET" should {

    "render the index page from a new instance of controller" in {
      val controller = new HomeController(stubControllerComponents())
      val home = controller.index().apply(FakeRequest(GET, "/"))

      status(home) mustBe OK
      contentType(home) mustBe Some("text/html")
      contentAsString(home) must include ("Welcome to Play")
    }

    "render the index page from the application" in {
      val controller = inject[HomeController]
      val home = controller.index().apply(FakeRequest(GET, "/"))

      status(home) mustBe OK
      contentType(home) mustBe Some("text/html")
      contentAsString(home) must include ("Welcome to Play")
    }

    "render the index page from the router" in {
      val request = FakeRequest(GET, "/")
      val home = route(app, request).get

      status(home) mustBe OK
      contentType(home) mustBe Some("text/html")
      contentAsString(home) must include ("Welcome to Play")
    }
  }
}

println("Play Framework examples defined successfully")

Summary

In this lesson, you've explored the rich Scala ecosystem:

Build Tools: SBT configuration, multi-module projects, and plugin integration
Configuration: Type-safe configuration management with Typesafe Config
Functional Libraries: Cats for functional programming abstractions
Effect Systems: Cats Effect for safe concurrency and resource management
Web Frameworks: Akka HTTP for reactive services and Play for full-stack apps
Testing: Integration testing and testing frameworks
Best Practices: Dependency injection, error handling, and production patterns

Understanding the ecosystem empowers you to choose the right tools and build production-ready Scala applications efficiently.

What's Next

In the next lesson, we'll explore advanced features and patterns including performance optimization, debugging techniques, and preparing Scala applications for production deployment.