Back to blog home

Lagom tutorial: using Event Sourcing to create an online shopping cart

This introductory tutorial to Lagom explores how to implement an ecommerce shopping cart using Lagom. You will need beginner-level experience with Scala.

Introduction

Lagom is a framework built on top of Akka and Play, with the intent of allowing users to easily and reliably build microservice-based applications.

To implement our online shopping cart in Lagom, we’re going to define three routes: one for adding products to the cart, one to see the contents of the cart, and one to remove a product from the cart. We’re going to wire those routes so that they will trigger queries, or commands, that will in turn trigger events.

By using Lagom, we’ll be able to abstract ourselves from the persistence of our entities and associated events, as well as from the details of the HTTP request / response architecture, by creating representations of our expected requests and responses using language types.

We’ll be doing this using Scala and sbt, so you’ll need to have these installed. We’ll be using curl to access our API endpoints since curl is widely available in a number of systems.

Setting up your Lagom project

To kickstart our Lagom shopping cart project, we’re going to use the g8 seed, as suggested in the Lagom documentation. By using the g8 seed, we’ll get a project that is pre-set-up for us, with a couple of example endpoints that we can use to check if everything is working. Another benefit is that a lot of the wiring code required to get Lagom up and running will already be put in place, freeing us up to start modeling our infrastructure and domain.

To set up a project using g8, we need to create a new project using sbt:

$ sbt new lagom/lagom-scala.g8

It will ask you a few questions about the project. Let’s enter the following details:

  • Name: ShoppingCart
  • Organisation: com.inviqa
  • Version: we can just use default
  • Package: com.inviqa.shoppingcart

When it finishes, we can step into the project folder and start working on it:

$ cd shoppingcart

We can run our newly created lagom application by calling sbt runAll from the command line. Let’s do that:

$ sbt runAll

If we look into the shoppingcart-api/src/main/scala/com/inviqa/shoppingcart/api/ShoppingcartService.scala file, we’ll see that there are two routes already defined with the path /api/hello/:id.

The first one is a GET endpoint, and takes only an id; and the second one, is a POST endpoint and will expect some JSON to be passed to it. That JSON is represented by the GreetingMessage case class.

Let’s test our newly set up application by calling one of the endpoints.

Keep sbt runAll running, and in another terminal window let’s use curl to make an HTTP request to our application:

$ curl http://localhost:9000/api/hello/World

We should see the following response:

‘Hello, World!’

We’ve now confirmed that our Lagom application is working. Let’s start writing our Shopping Cart application by defining our first API endpoint.

In order to have something visible, we need to be able to add a product to the shopping cart. For the purpose of this application, and for simplicity’s sake, we’re going to represent a product as a string. Let’s start our application.

Add a product to the shopping cart

Setting up the API service endpoint

Let’s start by defining a route for our endpoint. If we look at the existing endpoints, they’re defined using pathCall. Because we want to be more explicit about the type of endpoint, we’re going to use restCall instead of pathCall. With restCall, we can specify the type of request.

Inside the descriptor method, inside the call to withCalls, add a new line with the path for our ‘add products to our cart’ endpoint. We want our endpoint to be available through POST because we’re changing the shopping cart:

restCall(Method.POST, “/api/add-to-cart/:id”, addToCart _),

The first parameter is the request method for this endpoint, sometimes also called HTTP verb. The second is the path for the endpoint, and the third is a function that will be called when this endpoint is called. The _ after the function means that all the parameters that are captured during the call to this endpoint will be forwarded to the addToCart function.

In order for this line of code to work, we need to import Method, and declare the addToCartfunction.

Toward the top of the file where the other imports are, add the following import:

import com.lightbend.lagom.scaladsl.api.transport.Method

In this file, we’re just describing our API, so addToCart is going to be declared as abstract here in the ShoppingcartService trait.

def addProductToCart(id: String): ServiceCall[AddToCartRequest, Done]

So, what’s going on here? Our addToCart function takes a String which we’re naming id. This is the same id we’ve declared in the URL for our endpoint, and Lagom will make sure it is passed to our function. It then returns a value of type ServiceCall, parameterised by AddToCartRequest, which we haven’t created yet, and Done, which is provided by Akka.

If we look at the ServiceCall declaration, we’ll see it’s declared as follows:

trait ServiceCall[Request, Response]

So, this means that AddToCartRequest is something that represents the request, and Donerepresents the response. We’re using Done as return type here, because we don’t want to send any detailed response back.

AddToCartRequest will be a case class that represents the data format for the JSON we’re expecting to get in the body of the request when someone calls this endpoint. All we need to get is a product, and, for the sake of simplicity, the product will be identified by a string in this example:

case class AddToCartRequest(product: String)

Now it looks like we have everything we need to correctly compile our application. But, if you look at the compiler output, you’ll see the following error:

[error] shoppingcart/shoppingcart-api/src/main/scala/com/inviqa/shoppingcart/api/ShoppingcartService.scala:46: could not find implicit value for parameter requestSerializer: com.lightbend.lagom.scaladsl.api.deser.MessageSerializer[com.inviqa.shoppingcart.api.AddToCartRequest, _] │
[error] restCall(Method.POST, “/api/add-to-cart/:id”, addToCart _),

So what’s this about, then? This happens because Lagom doesn’t know how to convert the request body, which is a string, in JSON format, into the AddToCartRequest case class, so we need to define an encoder:

object AddToCartRequest {
  implicit val format: Format[AddToCartRequest] =
    Json.format[AddToCartRequest]
}

This fixed the previous compilation error, but now we have a new error. This time on a file we haven’t looked at yet.

[error] shoppingcart/shoppingcart-impl/src/main/scala/com/inviqa/shoppingcart/impl/ShoppingcartServiceImpl.scala:13: class ShoppingcartServiceImpl needs to be abstract, since method addToCart in trait ShoppingcartService of type (id: String)com.lightbend.lagom.scaladsl.api.ServiceCall[com.inviqa.shoppingcart.api.AddToCartRequest,akka.Done] is not defined
[error] class ShoppingcartServiceImpl(persistentEntityRegistry: PersistentEntityRegistry) extends ShoppingcartService {
[error] ^
[error] one error found

We get this error, because we’ve declared our AddToCart method as abstract in the ShoppingcartService trait, and now we need to create a concrete implementation for it in ShoppingcartServiceImpl which extends the ShoppingcartService trait.

So let’s leave our API file, and move to the folder where we keep our implementation.

Implementing the service

Open the com/inviqa/shoppingcart/impl/ShoppingcartServiceImpl.scala file. This file contains the implementation for our API serve. Add the implementation for addToCart here.

def addToCart(id: String): ServiceCall[AddToCartRequest, Done] = ServiceCall { request =>
  val ref = persistentEntityRegistry.refFor[ShoppingcartEntity]

  ref.ask(AddToCartCommand(request.product))}

And let’s import akka. Done:

import akka.Done

This is the concrete implementation of the method we defined as abstract in our API. What we’re doing here is loading the reference to the specific shopping cart to which we want to add the product. We do this by calling persistentEntityRegistry.refFor, parameterised with our ShoppingcartEntity, and passing the ID of the cart which we got from the URL call.

We then use that reference to ask it to run a command. In this case the AddToCartCommandwhich we haven’t declared yet. Because of this, we get an error: ‘not found: value AddToCartCommand’.

Handling the commands

We need to create this command in our ShoppingcartEntity, so let’s open the com/inviqa/shoppingcart/impl/ShoppingcartEntity.scala file and add this. Somewhere nearby the declaration of ShoppingcartCommand, which should have been added for us when we set up the project, let’s add:

final case class AddToCartCommand(product: String) extends ShoppingcartCommand[Done]

This is our custom command that gets called when we’re adding something to the cart. It extends the ShoppingcartCommand trait, parameterised with our response type, which is akka.Done.

Now our application compiles without issues. Let’s try to add a product to cart:

$ curl -X POST -d ‘{ “product”: “Apples” }’ -i -H “Content-Type: application/json” http://localhost:9000/api/add-to-cart/21
HTTP/1.1 500 Internal Server Error
Date: Mon, 12 Feb 2018 10:48:17 GMT
Server: akka-http/10.0.11
Content-Type: application/json
Content-Length: 457

{“name”:“com.lightbend.lagom.scaladsl.persistence.PersistentEntity$UnhandledCommandException: Unhandled command [com.inviqa.shoppingcart.impl.AddToCartCommand] in [com.inviqa.shoppingcart.impl.ShoppingcartEntity] with id [21]”,“detail”:“com.lightbend.lagom.scaladsl.persistence.PersistentEntity$UnhandledCommandException: Unhandled command [com.inviqa.shoppingcart.impl.AddToCartCommand] in [com.inviqa.shoppingcart.impl.ShoppingcartEntity] with id [21]\n”}

So, what’s happened here? Well, we’ve defined the command that will handle our ‘add to cart’ call, but we haven’t handled that command, and will need to do that.

In the ShoppingcartEntity class, there should be a behavior method which is handling the default example commands that were set up for us. Let’s clean up things up a bit so that we can see what we’re doing more clearly.

In com/inviqa/shoppingcart/api/ShoppingcartService.scala remove the helloand userGreeting abstract methods, along with the pathCalls, in the descriptormethod that maps to those two aforementioned methods.

We can now also remove the withTopics section, and the ShoppingcartService object, since we’re not using TOPIC_NAME anymore.

We can now also remove the greetingTopic method and the case classes and objects for GreetingMessage and GreetingMessageChanged.

Now our ShoppingcartService should look something like this:

package com.inviqa.shoppingcart.api

import akka.{Done, NotUsed}
import com.lightbend.lagom.scaladsl.api.transport.Method
import com.lightbend.lagom.scaladsl.api.{Service, ServiceCall}
import play.api.libs.json.{Format, Json}

/**

The ShoppingCart service interface.
<p>
This describes everything that Lagom needs to know about how to serve and
consume the ShoppingcartService.
*/
trait ShoppingcartService extends Service {
  def addToCart(id: String): ServiceCall[AddToCartRequest, Done]

  override final def descriptor = {
    import Service._
    // @formatter:off
    named(“shoppingcart”)
    .withCalls(
      restCall(Method.POST, “/api/add-to-cart/:id”, addToCart _)
    )
    .withAutoAcl(true)
    // @formatter:on
  }
}

case class AddToCartRequest(product: String)

object AddToCartRequest {
implicit val format: Format[AddToCartRequest] = Json.format[AddToCartRequest]
}

Now we need to clean up our implementation as well, as it depended on some of the case classes we’ve removed, and was implementing the abstract methods we’ve removed as well.

In the com/inviqa/shoppingcart/impl/ShoppingcartServiceImpl.scala file, let’s remove the hellouserGreetinggreetingTopic, and convertEvent methods. It should now look something like this:

package com.inviqa.shoppingcart.impl

import akka.Done
import com.inviqa.shoppingcart.api
import com.inviqa.shoppingcart.api.{AddToCartRequest, ShoppingcartService}
import com.lightbend.lagom.scaladsl.api.ServiceCall
import com.lightbend.lagom.scaladsl.persistence.{EventStreamElement, PersistentEntityRegistry}

/**

Implementation of the ShoppingcartService.
*/
class ShoppingcartServiceImpl(persistentEntityRegistry: PersistentEntityRegistry) extends ShoppingcartService {
  def addToCart(id: String): ServiceCall[AddToCartRequest, Done] = ServiceCall { request =>
    val ref = persistentEntityRegistry.refFor[ShoppingcartEntity]

    ref.ask(AddToCartCommand(request.product))
  }
}

Since we’re not focusing on streams here, let’s also remove the shoppingcart-stream-apiand shoppingcart-stream-impl folders. From the build.sbt file, also remove the lazy vals for the streams, and the file should look like this:

organization in ThisBuild := “com.inviqa”
version in ThisBuild := “1.0-SNAPSHOT”

// the Scala version that will be used for cross-compiled libraries
scalaVersion in ThisBuild := “2.12.4”

val macwire = “com.softwaremill.macwire” %% “macros” % “2.3.0” % “provided”
val scalaTest = “org.scalatest” %% “scalatest” % “3.0.4” % Test

lazy val shoppingcart = (project in file("."))
.aggregate(shoppingcart-api, shoppingcart-impl)

lazy val shoppingcart-api = (project in file(“shoppingcart-api”))
.settings(
  libraryDependencies ++= Seq(
    lagomScaladslApi
  )
)

lazy val shoppingcart-impl = (project in file(“shoppingcart-impl”))
.enablePlugins(LagomScala)
.settings(
  libraryDependencies ++= Seq(
    lagomScaladslPersistenceCassandra,
    lagomScaladslKafkaBroker,
    lagomScaladslTestKit,
    macwire,
    scalaTest
  )
)
.settings(lagomForkedTestSettings: _*)
.dependsOn(shoppingcart-api)

Stop sbt, run sbt clean, and then sbt runAll again. If you don’t do this, you may still get errors from missing files related to the folders we’ve just removed.

Now we can also clean the ShoppingcartEntity. So let’s edit the com/inviqa/shoppingcart/impl/ShoppingcartEntity.scala file and remove the command handlers and events for the endpoints we’ve just dropped, and all the commands and events we’re not using anymore.

Let’s remove GreetingMessageChanged – both the case class and object. Let’s also remove the UserGreetingMessage and Hello case class and object.

And in the ShoppingcartSerializerRegistry, let’s remove the serializer declarations for the removed case classes.

Let’s also replace the initial state with something more useful considering what we’re trying to do. Let’s initialise it with an empty cart. Our cart will be a list of products, so an empty cart will be an empty list.

Let’s make our ShoppnigcartState case class contain a list of products, and default it to empty. Remember, because our products are just string, our list will be a list of strings:

case class ShoppingcartState(products: List[String])

And in the ShopppngcartEntity class, override the initialState method:

override def initialState: ShoppingcartState = ShoppingcartState(List.empty)

And in our behavior method declaration, let’s replace this:

case ShoppingcartState(message, _) => Actions()

With this:

case ShoppingcartState(_) => Actions()

Now we still have the command and event handlers for the removed commands and events, so let’s remove them. And we should end up with code that looks something like this:

package com.inviqa.shoppingcart.impl

import akka.Done
import com.lightbend.lagom.scaladsl.persistence.{AggregateEvent, AggregateEventTag, PersistentEntity}
import com.lightbend.lagom.scaladsl.persistence.PersistentEntity.ReplyType
import com.lightbend.lagom.scaladsl.playjson.{JsonSerializer, JsonSerializerRegistry}
import play.api.libs.json.{Format, Json}

import scala.collection.immutable.Seq

class ShoppingcartEntity extends PersistentEntity {

  override type Command = ShoppingcartCommand[_]
  override type Event = ShoppingcartEvent
  override type State = ShoppingcartState

  /**

  The initial state. This is used if there is no snapshotted state to be found.
  */
  override def initialState: ShoppingcartState = ShoppingcartState(List.empty)
  /**

  An entity can define different behaviours for different states, so the behaviour
  is a function of the current state to a set of actions.
  */
  override def behavior: Behavior = {
    case ShoppingcartState(_) => Actions()
  }
}
/**

The current state held by the persistent entity.
*/
case class ShoppingcartState(products: List[String])
object ShoppingcartState {
  /**

  Format for the shopping cart state.
  Persisted entities get snapshotted every configured number of events. This
  means the state gets stored to the database, so that when the entity gets
  loaded, you don’t need to replay all the events, just the ones since the
  snapshot. Hence, a JSON format needs to be declared so that it can be
  serialized and deserialized when storing to and from the database.
  */
  implicit val format: Format[ShoppingcartState] = Json.format
}
/**

This interface defines all the events that the ShoppingcartEntity supports.
*/
sealed trait ShoppingcartEvent extends AggregateEvent[ShoppingcartEvent] {
def aggregateTag = ShoppingcartEvent.Tag
}
object ShoppingcartEvent {
  val Tag = AggregateEventTag[ShoppingcartEvent]
}

/**

This interface defines all the commands that the HelloWorld entity supports.
*/
sealed trait ShoppingcartCommand[R] extends ReplyType[R]
final case class AddToCartCommand(product: String) extends ShoppingcartCommand[Done]

/**

Akka serialization, used by both persistence and remoting, needs to have
serializers registered for every type serialized or deserialized. While it’s
possible to use any serializer you want for Akka messages, out of the box
Lagom provides support for JSON, via this registry abstraction.
The serializers are registered here, and then provided to Lagom in the
application loader.
*/
object ShoppingcartSerializerRegistry extends JsonSerializerRegistry {
  override def serializers: Seq[JsonSerializer[_]] = Seq(
    JsonSerializer[ShoppingcartState]
  )
}

We’re now back to a state where our code compiles, but we still don’t have a handler for the AddToCartCommand. Let’s fix that. First we handle the command in the behavior method of ShoppingcartEntity:

override def behavior: Behavior = {
  case ShoppingcartState(products) => Actions()
  .onCommand[AddToCartCommand, Done] {
  case (AddToCartCommand(product), context, state) =>
    context.thenPersist(
      AddedToCartEvent(product)
    ) { _ =>
      context.reply(Done)
    }
  }
}

And then we need to create the AddedToCartEvent case class to represent our new event. Near to the declaration of ShoppingcartState, add the case class to represent the event:

case class AddedToCartEvent(product: String) extends ShoppingcartEvent

Now the code compiles, and if we call our endpoint, we don’t get an error anymore:

$ curl -X POST -d ‘{ “product”: “Apples” }’ -i -H “Content-Type: application/json” http://localhost:9000/api/add-to-cart/21
HTTP/1.1 200 OK
Date: Mon, 12 Feb 2018 11:24:03 GMT
Server: akka-http/10.0.11
Content-Length: 0
Handling the event

We’re still not doing anything with the product or the cart, so we need to handle the event and write the logic to add a product to the cart. In the ShoppingcartEntity class, add the event handler to the behavior method:

override def behavior: Behavior = {
  case ShoppingcartState(_) => Actions()
  .onCommand[AddToCartCommand, Done] {
    case (AddToCartCommand(product), context, state) =>
    context.thenPersist(
      AddedToCartEvent(product)
    ) { _ =>
      context.reply(Done)
    }
  }.onEvent {
    case (AddedToCartEvent(product), state) =>
      ShoppingcartState(product :: state.products)
  }
}

Now, if we try calling the add to cart endpoint, we should get a ‘200 OK’ response code:

$ curl -X POST -d ‘{ “product”: “Apples” }’ -i -H “Content-Type: application/json” http://localhost:9000/api/add-to-cart/21
HTTP/1.1 200 OK
Date: Mon, 12 Feb 2018 11:47:53 GMT
Server: akka-http/10.0.11
Content-Length: 0

Seeing the contents of the shopping cart

We still have no way to confirm that the product was added to the cart, so let’s add an endpoint to see the contents of the cart.

Heading back to the API com/inviqa/shoppingcart/api/ShoppingcartService.scala let’s add the endpoint for getting the shopping cart contents. The process is similar to what we did for the add to cart endpoint, but this time we want the HTTP method to be GET:

def showCart(id: String): ServiceCall[NotUsed, List[String]]

override final def descriptor = {
  import Service._
  // @formatter:off
  named(“shoppingcart”)
  .withCalls(
    restCall(Method.POST, “/api/add-to-cart/:id”, addToCart _),
    restCall(Method.GET, “/api/cart/:id”, showCart _)
  )
  .withAutoAcl(true)
  // @formatter:on
  }
}

Similarly to what we did for the ‘add to cart’ endpoint, let’s add the implementation for showCart, in com/inviqa/shoppingcart/impl/ShoppingcartServiceImpl.scala:

def showCart(id: String): ServiceCall[NotUsed, List[String]] = ServiceCall { _ =>
  val ref = persistentEntityRegistry.refFor[ShoppingcartEntity]

  ref.ask(ShowCartCommand)
}

This follows the same pattern we’ve used for ‘add to cart’. We get the reference for the entity, and we call ask, with the command we want to execute. In this case we ignore the request as there’s no request body for a GET request.

Now we need to add a case class to represent and handle our command in the entity class. Let’s go back to com/inviqa/shoppingcart/impl/ShoppingcartEntity.scala and add a onReadOnlyCommand handler to the behavior method. A onReadOnlyCommand, is how Lagom refers to what’s usually called 'queries' in CQRS:

override def behavior: Behavior = {
  case ShoppingcartState(_) => Actions()
  .onCommand[AddToCartCommand, Done] {
    case (AddToCartCommand(product), context, state) =>
      context.thenPersist(
        AddedToCartEvent(product)
    ) { _ =>
      context.reply(Done)
    }
  }.onReadOnlyCommand[ShowCartCommand.type, List[String]] {
    case (ShowCartCommand, context, state) => context.reply(state.products)

  }.onEvent {
    case (AddedToCartEvent(product), state) =>
    ShoppingcartState(product :: state.products)
  }
}

Now let’s declare the ShowCartCommand case object to represent our command:

case object ShowCartCommand extends ShoppingcartCommand[List[String]]

If we query the cart now, we should get the products we stored in the cart:

$ curl http://localhost:9000/api/cart/21
[“Apples”]

Let’s add something else to the cart:

$ curl -X POST -d ‘{ “product”: “Pears” }’ -i -H “Content-Type: application/json” http://localhost:9000/api/add-to-cart/21
HTTP/1.1 200 OK
Date: Mon, 12 Feb 2018 12:15:58 GMT
Server: akka-http/10.0.11
Content-Length: 0

And now let’s add something else to a different cart:

$ curl -X POST -d ‘{ “product”: “Cherries” }’ -i -H “Content-Type: application/json” http://localhost:9000/api/add-to-cart/23
HTTP/1.1 200 OK
Date: Mon, 12 Feb 2018 12:16:06 GMT
Server: akka-http/10.0.11
Content-Length: 0

And if we now query both carts:

$ curl http://localhost:9000/api/cart/21
[“Pears”,“Apples”]
$ curl http://localhost:9000/api/cart/23
[“Cherries”]
Removing items from the shopping cart

Next, we’re going to remove products from the cart. For this, we’ll need another POST endpoint. Referring back to com/inviqa/shoppingcart/api/ShoppingcartService.scala, let’s add: the new route, an abstract method to handle the route, a case class to represent the request, and a companion object containing an implicit JSON encoder for the request. This is in every way similar to what we did for the add to cart endpoint. This will make the file look something like this:

package com.inviqa.shoppingcart.api

import akka.{Done, NotUsed}
import com.lightbend.lagom.scaladsl.api.transport.Method
import com.lightbend.lagom.scaladsl.api.{Service, ServiceCall}
import play.api.libs.json.{Format, Json}

/**

The ShoppingCart service interface.
<p>
This describes everything that Lagom needs to know about how to serve and
consume the ShoppingcartService.
*/
trait ShoppingcartService extends Service {
  def addToCart(id: String): ServiceCall[AddToCartRequest, Done]
  def showCart(id: String): ServiceCall[NotUsed, List[String]]
  def removeFromCart(id: String): ServiceCall[RemoveFromCartRequest, Done]

  override final def descriptor = {
    import Service._
    // @formatter:off
    named(“shoppingcart”)
    .withCalls(
      restCall(Method.POST, “/api/add-to-cart/:id”, addToCart _),
      restCall(Method.GET, “/api/cart/:id”, showCart _),
      restCall(Method.POST, “/api/cart/:id”, removeFromCart _)
    )
    .withAutoAcl(true)
    // @formatter:on
  }
}

case class AddToCartRequest(product: String)

object AddToCartRequest {
  implicit val format: Format[AddToCartRequest] = Json.format[AddToCartRequest]
}

case class RemoveFromCartRequest(product: String)

object RemoveFromCartRequest {
  implicit val format: Format[RemoveFromCartRequest] = Json.format[RemoveFromCartRequest]
}

Just as before, we now need a concrete implementation for removeFromCart in com/inviqa/shoppingcart/impl/ShoppingcartServiceImpl.scala. This will make our file look something like this:

package com.inviqa.shoppingcart.impl

import akka.{Done, NotUsed}
import com.inviqa.shoppingcart.api
import com.inviqa.shoppingcart.api.{AddToCartRequest, RemoveFromCartRequest, ShoppingcartService}
import com.lightbend.lagom.scaladsl.api.ServiceCall
import com.lightbend.lagom.scaladsl.persistence.{EventStreamElement, PersistentEntityRegistry}

/**

Implementation of the ShoppingcartService.
*/
class ShoppingcartServiceImpl(persistentEntityRegistry: PersistentEntityRegistry) extends ShoppingcartService {
  def addToCart(id: String): ServiceCall[AddToCartRequest, Done] = ServiceCall { request =>
    val ref = persistentEntityRegistry.refFor[ShoppingcartEntity]

    ref.ask(AddToCartCommand(request.product))
  }

  def showCart(id: String): ServiceCall[NotUsed, List[String]] = ServiceCall { _ =>
    val ref = persistentEntityRegistry.refFor[ShoppingcartEntity]

    ref.ask(ShowCartCommand)
  }

  def removeFromCart(id: String): ServiceCall[RemoveFromCartRequest, Done] = ServiceCall { request =>
    val ref = persistentEntityRegistry.refFor[ShoppingcartEntity]

    ref.ask(RemoveFromCartCommand(request.product))
  }
}

Now we need to add: a representation for the RemoveFromCartCommand, a handler for the commands, trigger an event from that command handler, and add a representation for that event. Our final com/inviqa/shoppingcart/impl/ShoppingcartEntity.scala file will look like this:

package com.inviqa.shoppingcart.impl

import akka.Done
import com.lightbend.lagom.scaladsl.persistence.{AggregateEvent, AggregateEventTag, PersistentEntity}
import com.lightbend.lagom.scaladsl.persistence.PersistentEntity.ReplyType
import com.lightbend.lagom.scaladsl.playjson.{JsonSerializer, JsonSerializerRegistry}
import play.api.libs.json.{Format, Json}

import scala.collection.immutable.Seq

class ShoppingcartEntity extends PersistentEntity {

  override type Command = ShoppingcartCommand[_]
  override type Event = ShoppingcartEvent
  override type State = ShoppingcartState

  /**

  The initial state. This is used if there is no snapshotted state to be found.
  */
  override def initialState: ShoppingcartState = ShoppingcartState(List.empty)
  /**

  An entity can define different behaviours for different states, so the behaviour

  is a function of the current state to a set of actions.
  */
  override def behavior: Behavior = {
    case ShoppingcartState(_) => Actions()
    .onCommand[AddToCartCommand, Done] {
      case (AddToCartCommand(product), context, state) =>
        context.thenPersist(
          AddedToCartEvent(product)
        ) { _ =>
          context.reply(Done)
        }
      }.onCommand[RemoveFromCartCommand, Done] {
        case (RemoveFromCartCommand(product), context, state) =>
          context.thenPersist(
            RemovedFromCartEvent(product)
          ) { _ =>
            context.reply(Done)
          }
      }.onReadOnlyCommand[ShowCartCommand.type, List[String]] {
        case (ShowCartCommand, context, state) => context.reply(state.products)

      }.onEvent {
        case (AddedToCartEvent(product), state) =>
          ShoppingcartState(product :: state.products)
        case (RemovedFromCartEvent(product), state) =>
          ShoppingcartState(state.products.filterNot(_ == product))
      }
  }
}

/**

The current state held by the persistent entity.
*/
case class ShoppingcartState(products: List[String])
object ShoppingcartState {
  /**

  Format for the shopping cart state.
  Persisted entities get snapshotted every configured number of events. This
  means the state gets stored to the database, so that when the entity gets
  loaded, you don’t need to replay all the events, just the ones since the
  snapshot. Hence, a JSON format needs to be declared so that it can be
  serialized and deserialized when storing to and from the database.
  */
  implicit val format: Format[ShoppingcartState] = Json.format
}
/**

This interface defines all the events that the ShoppingcartEntity supports.
*/
sealed trait ShoppingcartEvent extends AggregateEvent[ShoppingcartEvent] {
  def aggregateTag = ShoppingcartEvent.Tag
}
object ShoppingcartEvent {
  val Tag = AggregateEventTag[ShoppingcartEvent]
}

case class AddedToCartEvent(product: String) extends ShoppingcartEvent
case class RemovedFromCartEvent(product: String) extends ShoppingcartEvent

/**

This interface defines all the commands that the HelloWorld entity supports.
*/
sealed trait ShoppingcartCommand[R] extends ReplyType[R]
final case class AddToCartCommand(product: String) extends ShoppingcartCommand[Done]

case object ShowCartCommand extends ShoppingcartCommand[List[String]]

case class RemoveFromCartCommand(product: String) extends ShoppingcartCommand[Done]

/**

Akka serialization, used by both persistence and remoting, needs to have
serializers registered for every type serialized or deserialized. While it’s
possible to use any serializer you want for Akka messages, out of the box
Lagom provides support for JSON, via this registry abstraction.
The serializers are registered here, and then provided to Lagom in the
application loader.
*/
object ShoppingcartSerializerRegistry extends JsonSerializerRegistry {
  override def serializers: Seq[JsonSerializer[_]] = Seq(
    JsonSerializer[ShoppingcartState]
  )
}

The cart should now contain two products:

$ curl http://localhost:9000/api/cart/21
[“Pears”,“Apples”]

If we remove ‘Apples’ from the cart, we should be left with only the ‘Pears’:

$ curl -X POST -d ‘{ “product”: “Apples” }’ -i -H “Content-Type: application/json” http://localhost:9000/api/cart/21
HTTP/1.1 200 OK
Date: Mon, 12 Feb 2018 13:23:21 GMT
Server: akka-http/10.0.11
Content-Length: 0

$ curl http://localhost:9000/api/cart/21
[“Pears”]

Conclusion

And that’s it for this Lagom tutorial! I hope you’ve enjoyed the read and learnt something along the way. Here are some follow-up activities you can try to continue your learning:

  • Add implicit JSON formatters for events and commands so that the application can run in multiple instances in parallel
  • Replace the product string with a more realistic representation of a product
  • Support quantities in the shopping cart
  • Try adding 3 units of a product, and then removing 2 units from the cart
  • Only allow items from a catalogue of products to be added to the shopping cart (try adding or removing products from the catalogue)

Digital strategy consulting

Our award-winning web and software engineers are enabling businesses to solve their most complex technological challenges. So whether you’re looking to automate business processes, upskill your teams, or deliver better experiences, we can help.

Related reading

About the author

Marco Lopes is a technical team lead at Inviqa. He has been working as a software engineer for more that two decades, and is a programming language polyglot, with experience in such diverse languages as Scala, PHP, Java Perl, Python, and C#.