We're currently enhancing your experience. Our website is undergoing scheduled maintenance and UI improvements. Some features may be temporarily unavailable. We'll be back shortly. Thank you for your patience!
Back to Stories

Develop Event Driven Application Using ZIO Actors

A practical walkthrough for building an event-driven ticket booking system with ZIO Actors, Kafka, and Scala.

Develop Event Driven Application Using ZIO Actors

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.

What is ZIO?

ZIO provides a purely functional model for building concurrent and asynchronous applications on the JVM. It helps developers describe effects, compose workflows, manage resources safely, and build resilient applications with strong type safety.

Difference between Akka and ZIO

Akka and ZIO are both Scala libraries used for building concurrent, scalable, and fault-tolerant applications. They solve similar classes of problems, but they approach concurrency and composition differently.

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.

ZIO is a purely functional library that provides a type-safe and composable way to write concurrent and asynchronous code. It provides 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, teams may choose ZIO over Akka for a few practical reasons.

  • Type safety: ZIO uses the type system to enforce safe concurrency and parallelism, making errors easier to catch at compile time.
  • Lightweight: ZIO has a smaller API surface area and often requires less boilerplate code.
  • Performance: ZIO is optimized for low overhead, efficient thread management, and fine-grained control over scheduling and thread pools.
  • Compatibility: ZIO integrates well with existing Scala and Java libraries, which helps teams adopt it in existing projects.

ZIO Actors

In ZIO, actors are implemented as a type of fiber, which is a lightweight thread that can run concurrently with other fibers. An actor can receive messages and react to them.

To create an actor in ZIO, one approach is to use actor.make. This method takes a function that defines the behavior of the actor. The function receives the initial state of the actor and the message the actor receives.

Developing a Ticket Booking System using ZIO Actors

To create an actor system in ZIO, we need to add ZIO related dependencies in build.sbt. The following dependencies are used for the ticket booking system.

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

ZIO actor model and ticket booking system flow diagram
The ticket booking workflow uses Kafka, theatre actor, payment actor, booking sync actor, DynamoDB, and producer response flow.

Moving forward to the codebase, we introduce ZIO actors in a way that integrates a Kafka consumer and Kafka producer working in parallel with all actors. First, we define a main actor system 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 ()

  }

}

Initializing Kafka Consumer

Here, we initialize the Kafka consumer and pass booking information to the theatre actor using the tell method. The actor processes the data, fetches payment details, confirms the ticket, and passes control to the next actor.

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)

  }

TheatreActor implementation for TicketBookingSystem

This is one way to create an actor system, create multiple actors, and link them using ask or tell methods. In this code, the first actor is triggered from the Kafka consumer, and from there it can access the 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")

      }

  }

}

PaymentActor implementation for TicketBookingSystem

The theatre actor can call the payment actor. The payment actor receives booking data, prepares payment information, and returns the relevant booking message.

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")

      }

  }

}

bookingSyncActor implementation for TicketBookingSystem

The same theatre actor can also take the workflow to bookingSyncActor. This actor produces data through Kafka and can also sync information into downstream services such as DynamoDB.

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.

Sending a response back to client

For the reply message, the producer sends data on a different topic. This allows the system to respond asynchronously after the actor workflow completes.

 for {
 
           _ <- KafkaProducer.producerRun(value)

           //logic for db

          } yield((),())

Ticket Booking System Test Case

We can test actors using a simple unit test. For the theatreActor code, theatreActorSpec can check whether entered data reaches the actor correctly.

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

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 model domain-specific concurrency needs without the complexities of traditional actor systems.

With advanced features such as location transparency, message interception, and supervision, ZIO Actors simplifies the development and deployment of scalable and fault-tolerant distributed applications.

In this ticket booking system, we created theatre actor, payment actor, and booking sync actor. Each actor works according to its own logic, and the example shows how ZIO Actors can integrate with ZIO Kafka. Its integration with the ZIO ecosystem makes it easier to compose with other functional libraries and build robust, maintainable software.

Share this article: Twitter LinkedIn Email

Stay ahead of the curve.

Join our newsletter for weekly insights on technology, design, and the future of business.