Application Structure
Both libraries provide application templates for running complete programs.
Main Entry Point
import cats.effect._
// Simple app (no args)
object Main extends IOApp.Simple {
def run: IO[Unit] = for {
_ <- IO.println("Hello, World!")
_ <- IO.println("Application complete")
} yield ()
}
// With command-line args
object MainArgs extends IOApp {
def run(args: List[String]): IO[ExitCode] = for {
name <- IO.pure(args.headOption.getOrElse("World"))
_ <- IO.println(s"Hello, $name!")
} yield ExitCode.Success
}IOApp.Simple / IOApp
import zio._
// Simple app
object Main extends ZIOAppDefault {
def run: ZIO[Any, Any, Any] = for {
_ <- Console.printLine("Hello, World!")
_ <- Console.printLine("Application complete")
} yield ()
}
// With custom bootstrap
object MainCustom extends ZIOAppDefault {
override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] =
Runtime.removeDefaultLoggers
def run: ZIO[Any, Any, Any] =
Console.printLine("Custom runtime!")
}ZIOAppDefault / ZIOApp
Exit Codes
import cats.effect._
object MainExit extends IOApp {
def run(args: List[String]): IO[ExitCode] =
IO.pure(ExitCode.Success)
// Or with error
def runWithError(args: List[String]): IO[ExitCode] =
IO.raiseError[Unit](new Exception("fail"))
.as(ExitCode.Success)
.handleError(_ => ExitCode.Error)
}ExitCode.Success / ExitCode.Error
import zio._
object MainExit extends ZIOAppDefault {
// Return value determines exit code
def run: ZIO[Any, Any, Any] =
ZIO.succeed(()) // Exit code 0
// Or explicit exit
def runExplicit: ZIO[Any, Nothing, ExitCode] =
ZIO.succeed(ExitCode.success)
}Return type determines exit code
Bootstrap Configuration
import cats.effect._
import ciris._
import ciris.env._
case class Config(host: String, port: Int)
object MainConfig extends IOApp.Simple {
def run: IO[Unit] = for {
// Load config at startup (Ciris library)
config <- (
env("HOST").as[String].default("localhost"),
env("PORT").as[Int].default(8080)
).parMapN(Config.apply)
host <- IO.pure(config.host)
port <- IO.pure(config.port)
_ <- IO.println(s"Server: $host:$port")
} yield ()
}Load config with Ciris library
import zio._
import zio.config.magnolia._
import zio.config.typesafe.TypesafeConfigProvider._
case class AppConfig(host: String, port: Int)
object AppConfig {
implicit val config: zio.config.Config[AppConfig] =
deriveConfig[AppConfig]
}
object MainConfig extends ZIOAppDefault {
// Bootstrap: Configure runtime before app starts
override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] =
Runtime.setConfigProvider(fromHoconFilePath("config.conf")) ++
Runtime.removeDefaultLoggers
def run: ZIO[Any, Any, Any] =
for {
config <- ZIO.config[AppConfig]
host <- ZIO.succeed(config.host)
port <- ZIO.succeed(config.port)
_ <- ZIO.logInfo(s"Server: $host:$port")
} yield ()
}Bootstrap + ZIO.config
Bootstrap pattern: Override bootstrap in ZIOAppDefault to configure the runtime globally before your app runs. Common uses: set config provider, remove default loggers, add custom services.
Structured Logging
import cats.effect._
// Using log4cats (external library)
// import org.typelevel.log4cats.slf4j.Slf4jLogger
object MainLog extends IOApp.Simple {
def run: IO[Unit] = for {
// Console logging (built-in)
_ <- IO.println("[INFO] Application started")
_ <- IO.println("[DEBUG] Processing...")
_ <- IO.println("[INFO] Application complete")
} yield ()
// With log4cats:
// for {
// logger <- Slf4jLogger.create[IO]
// _ <- logger.info("Application started")
// } yield ()
}Console or log4cats (external)
import zio._
object MainLog extends ZIOAppDefault {
def run: ZIO[Any, Any, Any] = for {
// Built-in structured logging
_ <- ZIO.logInfo("Application started")
_ <- ZIO.logDebug("Processing...")
_ <- ZIO.logWarning("Potential issue")
_ <- ZIO.logError("Something went wrong")
} yield ()
}ZIO.log* - built-in logging
ZIO logging is built-in: No external library needed. ZIO.logInfo, ZIO.logDebug, ZIO.logWarning, and ZIO.logError provide structured logging with log levels and correlation IDs out of the box.
Resource Lifecycle in Apps
import cats.effect._
object MainResource extends IOApp.Simple {
def run: IO[Unit] = {
val dbResource = Resource.make(
IO.println("Opening DB") *> IO.pure("db-connection")
)(_ => IO.println("Closing DB"))
dbResource.use { db =>
IO.println(s"Using $db")
}
}
}Resource.use in IOApp
import zio._
object MainResource extends ZIOAppDefault {
def run: ZIO[Any, Any, Any] = ZIO.scoped {
for {
db <- ZIO.acquireRelease(
Console.printLine("Opening DB").as("db-connection")
)(_ => Console.printLine("Closing DB").orDie)
_ <- Console.printLine(s"Using $db")
} yield ()
}
}ZIO.scoped with acquireRelease
Service Access Patterns
import cats.effect._
trait Database {
def query: IO[Int]
}
trait Cache {
def get: IO[Option[Int]]
}
object MainServices extends IOApp.Simple {
def run: IO[Unit] = {
// Services passed as constructor parameters
def program(db: Database, cache: Cache): IO[Unit] =
for {
cached <- cache.get
result <- cached.fold(db.query)(IO.pure)
_ <- IO.println(s"Result: $result")
} yield ()
// Create services manually
val db: Database = new Database {
def query = IO.pure(42)
}
val cache: Cache = new Cache {
def get = IO.pure(Some(42))
}
program(db, cache)
}
}Constructor parameter injection
import zio._
trait Database {
def query: UIO[Int]
}
trait Cache {
def get: UIO[Option[Int]]
}
object MainServices extends ZIOAppDefault {
val dbLayer: ULayer[Database] = ZLayer.succeed(
new Database { def query = ZIO.succeed(42) }
)
val cacheLayer: ULayer[Cache] = ZLayer.succeed(
new Cache { def get = ZIO.succeed(Some(42)) }
)
def run: ZIO[Any, Any, Any] = {
val program: ZIO[Database & Cache, Nothing, Unit] = for {
// Service access: ZIO.serviceWithZIO
cached <- ZIO.serviceWithZIO[Cache](_.get)
result <- cached.fold(
ZIO.serviceWithZIO[Database](_.query)
)(ZIO.succeed(_))
_ <- Console.printLine(s"Result: $result").orDie
} yield ()
program.provide(dbLayer, cacheLayer)
}
}Service access with ZIO.serviceWithZIO
Service access operators:
ZIO.service[A]- Get the service (equivalent to constructor parameter)ZIO.serviceWithZIO[A](f)- Access service and run a ZIO operation (most common)ZIO.serviceWith[A](f)- Access service for pure transformation
Bootstrap Layer Composition
import cats.effect._
// Resource composition happens at runtime
object MainResources extends IOApp.Simple {
def run: IO[Unit] = {
// Each resource must be manually composed with nested use
Resource.make(IO.pure("db"))(_ => IO.unit).use { db =>
Resource.make(IO.pure("cache"))(_ => IO.unit).use { cache =>
IO.println(s"Using $db and $cache")
}
}
}
}Manual resource composition at runtime
import zio._
object MainBootstrap extends ZIOAppDefault {
// Bootstrap: Configure runtime before app starts
override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] =
Runtime.setConfigProvider(
ConfigProvider.envProvider
) ++
Runtime.removeDefaultLoggers
def run: ZIO[Any, Any, Any] = for {
_ <- ZIO.logInfo("App starting with custom bootstrap")
_ <- ZIO.logDebug("Debug logging enabled")
} yield ()
}Bootstrap layer composition (global)
Bootstrap runs before your app: The bootstrap layer initializes services globally for the entire application. Use it for services that should live for the full app lifetime (config, logging, database pools).
Next Steps
Application structure differs but both provide robust runtimes. Let's explore interop.