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