
What is ZIO :
ZIO is a cutting-edge framework for creating cloud-native JVM applications. ZIO enables developers to construct best-practice applications that are extremely scalable, tested, robust, resilient, resource-safe, efficient, and observable thanks to its user-friendly yet strong functional core.
Difference between Akka and ZIO :
Akka and ZIO are both the libraries in Scala for building concurrent, scalable, and fault-tolerant applications.
Akka is a toolkit and runtime for building highly concurrent, distributed, and fault-tolerant systems. It provides actors, which are lightweight units of computation that communicate with each other by exchanging messages. Akka also includes tools for clustering, routing, and persistence, making it well-suited for building reactive applications.
On the other hand, ZIO (ZIO stands for “ZIO Is Our”) is a purely functional library that provides a type-safe and composable way to write concurrent and asynchronous code. It provides a set of abstractions such as fibers, which are lightweight threads that can be composed to create complex applications, and effects, which are immutable and composable descriptions of side-effecting computations. ZIO also includes a powerful concurrency model and support for asynchronous IO operations.
Why ZIO over Akka :
ZIO and Akka are both powerful frameworks for building concurrent and distributed applications in Scala. However, there are some reasons why we might choose ZIO over Akka:
- Type safety: ZIO is a purely functional framework that uses the type system to enforce safe concurrency and parallelism. This makes it easier to reason about our code and catch errors at compile time rather than runtime.
- Lightweight: ZIO is a lightweight library that is easy to learn and use. It has a smaller API surface area than Akka and doesn’t require as much boilerplate code.
- Performance: ZIO is highly optimized for performance, with a low overhead for thread management and minimal context switching. It also provides fine-grained control over scheduling and thread pools.
- Compatibility: ZIO is fully compatible with existing Scala and Java libraries, making it easy to integrate into existing projects.
ZIO Actors :
In ZIO, actors are implemented as a type of fiber, which is a lightweight thread that can be run concurrently with other fibers. An actor is essentially a fiber that can receive messages and react to them.
To create an actor in ZIO, one of the way is we can use the actor.make
method, which takes a function that defines the behavior of the actor. The function takes two parameters: the first one is the initial state of the actor, and the second one is the message that the actor receives.
Developing a Ticket Booking System using ZIO Actors –
To create some actor system in ZIO, we need to add some ZIO related dependencies in build.sbt. Check below for the dependency which we have used-
libraryDependencies ++= Seq(
"dev.zio" %% "zio" % zioVersion,
"dev.zio" %% "zio-streams" % zioVersion,
"dev.zio" %% "zio-kafka" % "2.0.7",
"dev.zio" %% "zio-json" % "0.4.2",
"dev.zio" %% "zio-dynamodb" % "0.2.6",
"dev.zio" %% "zio-test" % zioVersion,
"dev.zio" %% "zio-actors" % "0.1.0",
"dev.zio" %% "zio-http" % "0.0.4",
"dev.zio" %% "zio-http-testkit" % "0.0.3",
"io.d11" %% "zhttp" % "2.0.0-RC11"
)
Architecture and Flow Diagram (Ticket Booking System) :

Then moving forward to codebase, we have introduced ZIO actor in such a way that there is an integration of kafka consumer, kafka producer which is is working parallely with all the actors.
Lets define a main actor for the TicketBookingSystem.
object TicketBookingSystem extends ZIOAppDefault {
val actorSystem = ActorSystem("ticketBookingSystem")
def run = {
println("starting actor system ")
for {
ticketInfoConsumerProducer <- KafkaConsumer.consumerRun.fork
_ <- ticketInfoConsumerProducer.join
} yield ()
}
}
Here, we are initializing Kafka Consumer.
def consumerRun: ZIO[Any, Throwable, Unit] = {
println("starting KafkaConsumer ")
val finalInfoStream =
Consumer
//create a kafka consumer here with respect to a particular topic
for {
theatreActor <- actorSystem.flatMap(x =>
x.make("ticketBookingflowActor", zio.actors.Supervisor.none, (),
theatreActor))
theatreActorData <- theatreActor ! ticketBooking
} yield theatreActorData
}
.map(_.offset)
.aggregateAsync(Consumer.offsetBatches)
.mapZIO(_.commit)
.drain
finalInfoStream.runDrain.provide(KafkaProdConsLayer.consumerLayer ++
KafkaProdConsLayer.producer)
}
In Kafka Consumer, we are passing booking information to the theatre actor by using tell method (!). This actor will process the data and fetch the payment details and confirm ticket and pass on to next actor.
TheatreActor implementation for TicketBookingSystem –
This is one of the way we can create actor system and various other actors and can link them using ask or tell method.In our code, we have added our first actor in kafka consumer itself, which is getting triggered. And from there, we can get access to our next actors.
object ThreatreActor {
val theatreActor: Stateful[Any, Unit, ZioMessage] = new Stateful[Any, Unit,
ZioMessage] {
override def receive[A](state: Unit, msg: ZioMessage[A], context:
Context): Task[(Unit, A)] =
msg match {
case BookingMessage(value) => {
println("ThreatreActor ................" + value)
val ticketConfirm= Booking(value.uuid, value.bookingDate,
value.theatreName, value.theatreLocation, value.seatNumbers,
value.cardNumber, value.pin,
value.cvv, value.otp, Some("Success"), Some("Confirmed"))
for{
paymentActor <- actorSystem.flatMap(x =>
x.make("paymentGatewayflowActor", zio.actors.Supervisor.none, (),
paymentGatewayflowActor))
paymentDetails <- paymentActor ? BookingMessage(value)
bookingSyncActor <- actorSystem.flatMap(x =>
x.make("bookingSyncActor", zio.actors.Supervisor.none, (), bookingSyncActor))
_ <- bookingSyncActor ! BookingMessage(ticketConfirm)
}yield {
println("Completed Theatre Actor")
((),())}
}
case _ => throw new Exception("Wrong value Input")
}
}
}
This code will take us to paymentActor which is written as below –
PaymentActor implementation for TicketBookingSystem –
object PaymentGatewayActor {
val paymentGatewayflowActor: Stateful[Any, Unit, ZioMessage] = new
Stateful[Any, Unit, ZioMessage] {
override def receive[A](state: Unit, msg: ZioMessage[A], context:
Context): Task[(Unit, A)] =
msg match {
case BookingMessage(value) =>
println("paymentInfo ................" + value)
val booking = Booking(value.uuid, value.bookingDate,
value.theatreName, value.theatreLocation, value.seatNumbers,
value.cardNumber, value.pin,
value.cvv, value.otp, Some("Success"), Some(""))
for {
bookingSyncActor <- actorSystem.flatMap(x =>
x.make("bookingSyncActor", zio.actors.Supervisor.none, (), bookingSyncActor))
//ZIO.succeed(booking)
}yield{
println("paymentInfo return................" + booking)
( BookingMessage(booking), ())
}
case _ => throw new Exception("Wrong value Input")
}
}
}
The same theatreActor will take us to bookingSyncActor which is written as below –
bookingSyncActor implementation for TicketBookingSystem –
val bookingSyncActor: Stateful[Any, Unit, ZioMessage] = new Stateful[Any,
Unit, ZioMessage] {
override def receive[A](state: Unit, msg: ZioMessage[A], context:
Context): Task[(Unit, A)] =
msg match {
case BookingMessage(value) =>
println("bookingSyncActor ................" + value)
for {
_ <- KafkaProducer.producerRun(value)
_ <- f1(value).provide(
netty.NettyHttpClient.default,
config.AwsConfig.default,
dynamodb.DynamoDb.live,
DynamoDBExecutor.live
)
}yield((),())
}
} // plus some other computations.
These different actors will collect some info from the base case class and will give some relevant info regarding the individual actors.
Sending a response back to client –
For reply message, the producer is producing the data on a different topic (reply message)
for {
_ <- KafkaProducer.producerRun(value)
//logic for db
} yield((),())
Ticket Booking System Test Case :
Sure, Lets check how to test our actors using unit test.
For the above theatreActor code, we have created theatreActorSpec which can be written in this simple format. We can check if the data entered is getting correct in the actor or not.
object ThreatreActorSpec extends ZIOAppDefault{
val data: Booking = booking.handler.actor.JsonSampleData.booking
override def run: ZIO[Any with ZIOAppArgs with Scope, Any, Any] =
for {
system <- ActorSystem("ticketBookingSystem")
actor <- system.make("ticketBookingflowActor", Supervisor.none, (),
theatreActor)
result <- actor ! BookingMessage(data)
}
yield result
}
To run this service –
- Setup broker
- Run Actor Service
- Run Producer
Conclusion :
In conclusion, ZIO Actors is a powerful and efficient library for building concurrent and distributed systems in Scala. It provides a lightweight and type-safe approach to concurrency, allowing developers to easily model their domain-specific concurrency needs without the complexities of traditional actor systems. With its advanced features such as location transparency, message interception, and supervision, ZIO Actors simplifies the development and deployment of highly scalable and fault-tolerant distributed applications.
Here, we created various actors such as theatre actor, payment actor and booking sync actor which are working as per their logic. Also, we tried to implement ZIO actors with ZIO kafka and this is a very good example of this simple integration.
Additionally, its integration with the ZIO ecosystem allows for seamless composition with other functional libraries, providing developers with a powerful and cohesive toolkit for building robust and maintainable software. For more blogs, please refer here . For more ZIO blogs, refer here
