Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Project: Bookstore

To create an Akka Actor-based version of the bookstore, we need to use Akka to manage concurrency and message passing between different components of the application. Below is an example implementation.

Step 1: Add Akka Dependencies

First, add Akka dependencies to your build.sbt file:

libraryDependencies += "com.typesafe.akka" %% "akka-actor-typed" % "2.6.18"

Step 2: Define the Project Structure

Create directories and files as follows:

src/main/scala/
├── models/
│   ├── Book.scala
│   ├── Customer.scala
│   └── Order.scala
├── actors/
│   ├── BookActor.scala
│   ├── CustomerActor.scala
│   └── OrderActor.scala
└── Main.scala

Step 3: Define Models

models/Book.scala

package models

case class Book(id: Int, title: String, author: String, price: Double, stock: Int)

models/Customer.scala

package models

case class Customer(id: Int, name: String, email: String, address: String)

models/Order.scala

package models

case class Order(id: Int, customerId: Int, bookId: Int, quantity: Int, status: String)

Step 4: Implement Actors

actors/BookActor.scala

package actors

import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.{ActorRef, Behavior}
import models.Book

object BookActor {
  sealed trait Command
  case class AddBook(title: String, author: String, price: Double, stock: Int, replyTo: ActorRef[Response]) extends Command
  case class ListBooks(replyTo: ActorRef[Response]) extends Command
  case class FindBookById(id: Int, replyTo: ActorRef[Response]) extends Command
  case class UpdateStock(id: Int, newStock: Int, replyTo: ActorRef[Response]) extends Command

  sealed trait Response
  case class BookAdded(book: Book) extends Response
  case class BooksListed(books: List[Book]) extends Response
  case class BookFound(book: Option[Book]) extends Response
  case class StockUpdated(book: Option[Book]) extends Response

  def apply(): Behavior[Command] = {
    var books: Map[Int, Book] = Map.empty
    var nextBookId: Int = 1

    Behaviors.receiveMessage {
      case AddBook(title, author, price, stock, replyTo) =>
        val book = Book(nextBookId, title, author, price, stock)
        books += (nextBookId -> book)
        nextBookId += 1
        replyTo ! BookAdded(book)
        Behaviors.same

      case ListBooks(replyTo) =>
        replyTo ! BooksListed(books.values.toList)
        Behaviors.same

      case FindBookById(id, replyTo) =>
        replyTo ! BookFound(books.get(id))
        Behaviors.same

      case UpdateStock(id, newStock, replyTo) =>
        val updatedBook = books.get(id).map(book => book.copy(stock = newStock))
        updatedBook.foreach(book => books += (id -> book))
        replyTo ! StockUpdated(updatedBook)
        Behaviors.same
    }
  }
}

actors/CustomerActor.scala

package actors

import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.{ActorRef, Behavior}
import models.Customer

object CustomerActor {
  sealed trait Command
  case class AddCustomer(name: String, email: String, address: String, replyTo: ActorRef[Response]) extends Command
  case class ListCustomers(replyTo: ActorRef[Response]) extends Command
  case class FindCustomerById(id: Int, replyTo: ActorRef[Response]) extends Command

  sealed trait Response
  case class CustomerAdded(customer: Customer) extends Response
  case class CustomersListed(customers: List[Customer]) extends Response
  case class CustomerFound(customer: Option[Customer]) extends Response

  def apply(): Behavior[Command] = {
    var customers: Map[Int, Customer] = Map.empty
    var nextCustomerId: Int = 1

    Behaviors.receiveMessage {
      case AddCustomer(name, email, address, replyTo) =>
        val customer = Customer(nextCustomerId, name, email, address)
        customers += (nextCustomerId -> customer)
        nextCustomerId += 1
        replyTo ! CustomerAdded(customer)
        Behaviors.same

      case ListCustomers(replyTo) =>
        replyTo ! CustomersListed(customers.values.toList)
        Behaviors.same

      case FindCustomerById(id, replyTo) =>
        replyTo ! CustomerFound(customers.get(id))
        Behaviors.same
    }
  }
}

actors/OrderActor.scala

package actors

import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.{ActorRef, Behavior}
import models.{Order, Book, Customer}

object OrderActor {
  sealed trait Command
  case class PlaceOrder(customerId: Int, bookId: Int, quantity: Int, replyTo: ActorRef[Response]) extends Command
  case class ListOrders(replyTo: ActorRef[Response]) extends Command
  case class ListOrdersByCustomer(customerId: Int, replyTo: ActorRef[Response]) extends Command

  sealed trait Response
  case class OrderPlaced(order: Option[Order]) extends Response
  case class OrdersListed(orders: List[Order]) extends Response
  case class OrdersByCustomerListed(orders: List[Order]) extends Response

  def apply(bookActor: ActorRef[BookActor.Command], customerActor: ActorRef[CustomerActor.Command]): Behavior[Command] = {
    var orders: List[Order] = List.empty
    var nextOrderId: Int = 1

    Behaviors.receiveMessage {
      case PlaceOrder(customerId, bookId, quantity, replyTo) =>
        val order = Order(nextOrderId, customerId, bookId, quantity, "Placed")
        orders = orders :+ order
        nextOrderId += 1
        replyTo ! OrderPlaced(Some(order))
        Behaviors.same

      case ListOrders(replyTo) =>
        replyTo ! OrdersListed(orders)
        Behaviors.same

      case ListOrdersByCustomer(customerId, replyTo) =>
        replyTo ! OrdersByCustomerListed(orders.filter(_.customerId == customerId))
        Behaviors.same
    }
  }
}

Step 5: Create the Main Application

Main.scala

import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.{ActorSystem, Behavior}
import actors.{BookActor, CustomerActor, OrderActor}
import scala.util.{Try, Success, Failure}
import utils.Utils._

object Main extends App {
  sealed trait Command
  case class Run() extends Command

  def apply(): Behavior[Command] = Behaviors.setup { context =>
    val bookActor = context.spawn(BookActor(), "BookActor")
    val customerActor = context.spawn(CustomerActor(), "CustomerActor")
    val orderActor = context.spawn(OrderActor(bookActor, customerActor), "OrderActor")

    def run(): Unit = {
      var continue = true
      while (continue) {
        println("\nBookstore App")
        println("1. Add Book")
        println("2. List Books")
        println("3. Add Customer")
        println("4. List Customers")
        println("5. Place Order")
        println("6. List Orders")
        println("7. List Orders by Customer")
        println("8. Exit")
        print("Choose an option: ")

        scala.io.StdIn.readLine() match {
          case "1" => addBook(bookActor)
          case "2" => listBooks(bookActor)
          case "3" => addCustomer(customerActor)
          case "4" => listCustomers(customerActor)
          case "5" => placeOrder(orderActor)
          case "6" => listOrders(orderActor)
          case "7" => listOrdersByCustomer(orderActor)
          case "8" => continue = false
          case _ => println("Invalid option. Please try again.")
        }
      }
    }

    def addBook(bookActor: ActorRef[BookActor.Command]): Unit = {
      val title = readLineWithPrompt("Enter book title: ")
      val author = readLineWithPrompt("Enter book author: ")
      val priceTry = readDoubleWithPrompt("Enter book price: ")
      val stockTry = readIntWithPrompt("Enter book stock: ")

      (priceTry, stockTry) match {
        case (Success(price), Success(stock)) =>
          bookActor ! BookActor.AddBook(title, author, price, stock, context.self)
          println(s"Book added: $title by $author")
        case (Failure(priceEx), _) =>
          println(s"Invalid price input. Error: ${priceEx.getMessage}")
        case (_, Failure(stockEx)) =>
          println(s"Invalid stock input. Error: ${stockEx.getMessage}")
      }
    }

    def listBooks(bookActor: ActorRef[BookActor.Command]): Unit = {
      bookActor ! BookActor.ListBooks(context.self)
    }

    def addCustomer(customerActor: ActorRef[CustomerActor.Command]): Unit = {
      val name = readLineWithPrompt("Enter customer name: ")
      val email = readLineWithPrompt("Enter customer email: ")
      val address = readLineWithPrompt("Enter customer address: ")
      customerActor ! CustomerActor.AddCustomer(name, email, address, context.self)
      println(s"Customer added: $name")
    }

    def listCustomers(customerActor: ActorRef[CustomerActor.Command]): Unit = {
      customerActor ! CustomerActor.ListCustomers(context.self)
    }

    def placeOrder(orderActor: ActorRef[OrderActor.Command]): Unit = {
      val customerIdTry = readIntWithPrompt("Enter customer id: ")
      val bookIdTry = readIntWithPrompt("Enter book id: ")
      val quantityTry = readIntWithPrompt("Enter quantity: ")

      (customerIdTry, bookIdTry, quantityTry) match {
        case (Success(customerId), Success(bookId), Success(quantity)) =>
          orderActor ! OrderActor.PlaceOrder(customerId, bookId, quantity, context.self)
          println(s"Order placed: Customer $customerId ordered Book $bookId")
        case (Failure(customerIdEx), _, _) =>
          println(s"Invalid customer id input. Error: ${customerIdEx.getMessage}")
        case (_, Failure(bookIdEx), _) =>
          println(s"Invalid book id input. Error: ${bookIdEx.getMessage}")
        case (_, _, Failure(quantityEx)) =>
          println(s"Invalid quantity input. Error: ${quantityEx.getMessage}")
      }
    }

    def listOrders(orderActor: ActorRef[OrderActor.Command]): Unit = {
      orderActor ! OrderActor.ListOrders(context.self)
    }

    def listOrdersByCustomer(orderActor: ActorRef[OrderActor.Command]): Unit = {
      val customerIdTry = readIntWithPrompt("Enter customer id: ")
      customerIdTry match {
        case Success(customerId) =>
          orderActor ! OrderActor.ListOrdersByCustomer(customerId, context.self)
        case Failure(ex) =>
          println(s"Invalid customer id input. Error: ${ex.getMessage}")
      }
    }

    Behaviors.receiveMessage {
      case Run() =>
        run()
        Behaviors.same
    }
  }

  val system = ActorSystem(Main(), "BookstoreSystem")
  system ! Run()
}

Step 6: Implement Utility Functions

utils/Utils.scala

package utils

import scala.util.{Try, Success, Failure}

object Utils {
  def readLineWithPrompt(prompt: String): String = {
    print(prompt)
    scala.io.StdIn.readLine()
  }

  def readIntWithPrompt(prompt: String): Try[Int] = {
    print(prompt)
    Try(scala.io.StdIn.readInt())
  }

  def readDoubleWithPrompt(prompt: String): Try[Double] = {
    print(prompt)
    Try(scala.io.StdIn.readDouble())
  }
}

Running Your Akka Actor-based Application

To run your application, use SBT:

sbt run