blob: 6a92d1ce732832cf593e503f15268f19d59c832b [file] [log] [blame]
= Section 5: Complete Event Sourced entity
:page-supergroup-java-scala: Language
include::ROOT:partial$include.adoc[]
On this page, we will complete the cart entity with more commands and events. This `ShoppingCart` entity will use Event Sourcing to persist events that represent changes to the state of the cart.
.This part of the xref:overview.adoc[full example] will focus on the shopping cart entity.
[caption=""]
image::example-entity.svg[Example entity]
On this page you will learn how to:
* implement an Event Sourced entity by expanding work from the xref:entity.adoc[previous steps] and adding to the `ShoppingCart`:
** `Checkout` - a command to checkout the shopping cart
** `CheckedOut` - an event to capture checkouts
** `Get` - a way get the current state of the shopping cart
At the end, we also provide a list of <<Optional commands and events>> that you can add on your own to test your knowledge.
== Source downloads
If you prefer to simply view and run the example, download a zip file containing the completed code:
[.tabset]
Java::
+
****
* link:_attachments/2-shopping-cart-event-sourced-java.zip[Source] that includes all previous tutorial steps and allows you to start with the steps on this page.
* link:_attachments/3-shopping-cart-event-sourced-complete-java.zip[Source] with the steps on this page completed.
****
Scala::
+
****
* link:_attachments/2-shopping-cart-event-sourced-scala.zip[Source] that includes all previous tutorial steps and allows you to start with the steps on this page.
* link:_attachments/3-shopping-cart-event-sourced-complete-scala.zip[Source] with the steps on this page completed.
****
:sectnums:
== Add the command and event for checkout
When the cart has been checked out it should not accept any commands that change its state. However, it should still be possible to `Get` the current state of a checked out cart. We suggest you try implementing `Checkout` on your own and then compare it with the solution shown below. Add the `Checkout` command alongside the existing `AddItem` command.
The following sections show our solution.
=== Checkout command
[.tabset]
Java::
+
.src/main/java/shopping/cart/ShoppingCart.java
[source,java,indent=0]
----
include::example$03-shopping-cart-service-java/src/main/java/shopping/cart/ShoppingCart.java[tag=checkoutCommand]
----
Scala::
+
.src/main/scala/shopping/cart/ShoppingCart.scala
[source,scala,indent=0]
----
include::example$03-shopping-cart-service-scala/src/main/scala/shopping/cart/ShoppingCart.scala[tag=checkoutCommand]
----
=== CheckedOut event
The corresponding `CheckedOut` event:
[.tabset]
Java::
+
.src/main/java/shopping/cart/ShoppingCart.java
[source,java,indent=0]
----
include::example$03-shopping-cart-service-java/src/main/java/shopping/cart/ShoppingCart.java[tag=checkedOutEvent]
----
Scala::
+
.src/main/scala/shopping/cart/ShoppingCart.scala
[source,scala,indent=0]
----
include::example$03-shopping-cart-service-scala/src/main/scala/shopping/cart/ShoppingCart.scala[tag=checkedOutEvent]
----
=== State changes
The state should include a value for if and when the cart was checked out:
[.tabset]
Java::
+
.src/main/java/shopping/cart/ShoppingCart.java
[source,java,indent=0]
----
include::example$03-shopping-cart-service-java/src/main/java/shopping/cart/ShoppingCart.java[tag=state-with-checkout]
----
Scala::
+
.src/main/scala/shopping/cart/ShoppingCart.scala
[source,scala,indent=0]
----
include::example$03-shopping-cart-service-scala/src/main/scala/shopping/cart/ShoppingCart.scala[tag=state-with-checkout]
----
=== Unit test
Add a unit test for the new `Checkout` command in [.group-scala]#`ShoppingCartSpec`# [.group-java]#`ShoppingCartTest`#:
[.tabset]
Java::
+
.src/test/java/shopping/cart/ShoppingCartTest.java
[source,java,indent=0]
----
include::example$03-shopping-cart-service-java/src/test/java/shopping/cart/ShoppingCartTest.java[tag=checkout]
----
Scala::
+
.src/test/scala/shopping/cart/ShoppingCartSpec.scala
[source,scala,indent=0]
----
include::example$03-shopping-cart-service-scala/src/test/scala/shopping/cart/ShoppingCartSpec.scala[tag=checkout]
----
You will also have to update the "add item" to use the new `Summary` signature.
Commands should be handled differently when the cart has been checked out. `AddItem` is no longer allowed after checkout. Therefore, we refactor the [.group-scala]#`handleCommand`# [.group-java]#`commandHandler`# method into two separate methods `openShoppingCart` and `checkedOutShoppingCart` that are used depending on the `checkedOut` state. The previous code for `AddItem` goes into the `openShoppingCart` method as well as the new `Checkout` command.
[.tabset]
Java::
+
.src/main/java/shopping/cart/ShoppingCart.java
[source,java,indent=0]
----
include::example$03-shopping-cart-service-java/src/main/java/shopping/cart/ShoppingCart.java[tag=commandHandlers]
----
Scala::
+
.src/main/scala/shopping/cart/ShoppingCart.scala
[source,scala,indent=0]
----
include::example$03-shopping-cart-service-scala/src/main/scala/shopping/cart/ShoppingCart.scala[tag=commandHandlers]
----
In `checkedOutShoppingCart` the `AddItem` and `Checkout` commands should be rejected:
[.tabset]
Java::
+
.src/main/java/shopping/cart/ShoppingCart.java
[source,java,indent=0]
----
include::example$03-shopping-cart-service-java/src/main/java/shopping/cart/ShoppingCart.java[tag=checkedOutShoppingCart]
----
Scala::
+
.src/main/scala/shopping/cart/ShoppingCart.scala
[source,scala,indent=0]
----
include::example$03-shopping-cart-service-scala/src/main/scala/shopping/cart/ShoppingCart.scala[tag=checkedOutShoppingCart]
----
=== Event handler
We still need to add the event handler for the `CheckedOut` event in the `handleEvent` method:
[.tabset]
Java::
+
.src/main/java/shopping/cart/ShoppingCart.java
[source,java,indent=0]
----
include::example$03-shopping-cart-service-java/src/main/java/shopping/cart/ShoppingCart.java[tag=checkedOutEventHandler]
----
Scala::
+
.src/main/scala/shopping/cart/ShoppingCart.scala
[source,scala,indent=0]
----
include::example$03-shopping-cart-service-scala/src/main/scala/shopping/cart/ShoppingCart.scala[tag=checkedOutEventHandler]
----
=== Run unit tests
That should cover everything for the `Checkout` command. If you shut down the service after the previous exercise you will need to restart it using `docker-compose up -d` in the command line.
Let's confirm everything by running the unit tests with:
[.group-scala]
[source,shell script]
----
sbt test
----
[.group-java]
[source,shell script]
----
mvn test
----
== Add Get command
Add the `Get` command alongside the existing `AddItem` and `Checkout` commands:
[.tabset]
Java::
+
.src/main/java/shopping/cart/ShoppingCart.java
[source,java,indent=0]
----
include::example$03-shopping-cart-service-java/src/main/java/shopping/cart/ShoppingCart.java[tag=getCommand]
----
Scala::
+
.src/main/scala/shopping/cart/ShoppingCart.scala
[source,scala,indent=0]
----
include::example$03-shopping-cart-service-scala/src/main/scala/shopping/cart/ShoppingCart.scala[tag=getCommand]
----
Add a unit test for the new `Get` command in `ShoppingCartSpec`:
[.tabset]
Java::
+
.src/test/java/shopping/cart/ShoppingCartTest.java
[source,java,indent=0]
----
include::example$03-shopping-cart-service-java/src/test/java/shopping/cart/ShoppingCartTest.java[tag=get]
----
Scala::
+
.src/test/scala/shopping/cart/ShoppingCartSpec.scala
[source,scala,indent=0]
----
include::example$03-shopping-cart-service-scala/src/test/scala/shopping/cart/ShoppingCartSpec.scala[tag=get]
----
The command handler for `Get` is independent of the `checkedOut` state, [.group-scala]#so add it to both `openShoppingCart` and `checkedOutShoppingCart`# [.group-java]#so it can be added to the command handler builder using `forAnyState()`#:
[.tabset]
Java::
+
.src/main/java/shopping/cart/ShoppingCart.java
[source,java,indent=0]
----
include::example$03-shopping-cart-service-java/src/main/java/shopping/cart/ShoppingCart.java[tag=getCommandHandler]
----
Scala::
+
.src/main/scala/shopping/cart/ShoppingCart.scala
[source,scala,indent=0]
----
include::example$03-shopping-cart-service-scala/src/main/scala/shopping/cart/ShoppingCart.scala[tag=getCommandHandler]
----
Try the new `Get` command by running the unit tests with:
[.group-scala]
[source,shell script]
----
sbt test
----
[.group-java]
[source,shell script]
----
mvn test
----
== Add new operations to the service descriptor
In the existing `ShoppingCartService.proto` add corresponding operation definitions in the form of the two rpc calls listed below:
.src/main/protobuf/ShoppingCartService.proto
[source,protobuf]
----
include::example$03-shopping-cart-service-scala/src/main/protobuf/ShoppingCartService.proto[tag=CheckoutAndGet]
----
<1> Defines the `Checkout` operation.
<2> Defines the `GetCart` operation.
<3> For simplicity, most requests share a common response, for easier evolution of an interface, separate responses are often a better choice.
<4> Note the new `checkedOut` flag.
Generate code by compiling the project:
[.group-scala]
[source,shell script]
----
sbt compile
----
[.group-java]
[source,shell script]
----
mvn compile
----
You will see a compilation error in [.group-scala]#`ShoppingCartServiceImpl.scala`# [.group-java]#`ShoppingCartServiceImpl.java`#, but that is expected with the changed Protobuf definition. We will fix that now.
Add implementations of the new operations in `ShoppingCartServiceImpl` in the same way as `addItem`:
[.tabset]
Java::
+
.src/main/java/shopping/cart/ShoppingCartServiceImpl.java
[source,java,indent=0]
----
include::example$03-shopping-cart-service-java/src/main/java/shopping/cart/ShoppingCartServiceImpl.java[tag=checkoutAndGet]
----
Scala::
+
.src/main/scala/shopping/cart/ShoppingCartServiceImpl.scala
[source,scala,indent=0]
----
include::example$03-shopping-cart-service-scala/src/main/scala/shopping/cart/ShoppingCartServiceImpl.scala[tag=checkoutAndGet]
----
We also have to include the new `checkedOut` flag when converting from `ShoppingCart.Summary` to `proto.Cart`.
[.tabset]
Java::
+
.src/main/java/shopping/cart/ShoppingCartServiceImpl.java
[source,java,indent=0]
----
include::example$03-shopping-cart-service-java/src/main/java/shopping/cart/ShoppingCartServiceImpl.java[tag=toProtoCart]
----
Scala::
+
.src/main/scala/shopping/cart/ShoppingCartServiceImpl.scala
[source,scala,indent=0]
----
include::example$03-shopping-cart-service-scala/src/main/scala/shopping/cart/ShoppingCartServiceImpl.scala[tag=toProtoCart]
----
== Run locally
Start the PostgresSQL database, unless it's already running:
[source,shell script]
----
docker-compose up -d
----
Run the service with:
[.group-scala]
[source,shell script]
----
sbt -Dconfig.resource=local1.conf run
----
[.group-java]
[source,shell script]
----
# make sure to compile before running exec:exec
mvn compile exec:exec -DAPP_CONFIG=local1.conf
----
=== Exercise the service
// # tag::exercise[]
Use `https://github.com/fullstorydev/grpcurl[grpcurl]` to exercise the service:
. Add an item to the cart:
+
[source,shell script]
----
grpcurl -d '{"cartId":"cart2", "itemId":"socks", "quantity":3}' -plaintext 127.0.0.1:8101 shoppingcart.ShoppingCartService.AddItem
----
. Check the quantity of the cart:
+
[source,shell script]
----
grpcurl -d '{"cartId":"cart2"}' -plaintext 127.0.0.1:8101 shoppingcart.ShoppingCartService.GetCart
----
. Check out cart:
+
[source,shell script]
----
grpcurl -d '{"cartId":"cart2"}' -plaintext 127.0.0.1:8101 shoppingcart.ShoppingCartService.Checkout
----
// # end::exercise[]
=== Stop the service
When finished, stop the service with `ctrl-c`. Leave PostgresSQL running for the next set of steps, or stop it with:
[source,shell script]
----
docker-compose stop
----
NOTE: The following steps for cloud deployment are optional. If you are only running locally, you can skip to the next section of the tutorial.
[#kubernetes]
== Run in Kubernetes
Create a xref:deployment:aws-install.adoc[Kubernetes cluster and install the Akka Operator] if you haven't already.
=== Create a PostgresSQL database
Make sure you have created your database and loaded the schema as described in the previous tutorial section.
=== Build Docker image
Create a Docker repository and authenticate Docker.
[.tabset]
GCP::
+
Follow the instructions in https://cloud.google.com/container-registry/docs/using-with-google-cloud-platform[Using Container Registry with Google Cloud {tab-icon}, window="tab"] to deploy Docker images on GCP's container registry.
AWS::
+
Follow the instructions in xref:deployment:aws-ecr.adoc[Amazon Elastic Container Registry] to deploy Docker images on AWS's container registry.
=== Additional steps for Docker and AWS
If you are using AWS, you will also need to complete the following procedures.
Rebuild the Docker image.
include::partial$build-docker-for-kube.adoc[]
=== Update the deployment descriptor
Update the `kubernetes/shopping-cart-service-cr.yml` deployment descriptor with the new image tag as previously described.
=== Apply to Kubernetes
Re-apply the `shopping-cart-service-cr.yml` to Kubernetes:
[source,shell script]
----
kubectl apply -f kubernetes/shopping-cart-service-cr.yml
----
You can see progress by viewing the status:
[source,shell script]
----
kubectl get akkamicroservices/shopping-cart-service
----
See xref:deployment:troubleshooting.adoc[troubleshooting deployment status] for more details.
=== Exercise the service in Kubernetes
include::partial$prepare-to-exercise-in-kube.adoc[]
include::complete-entity.adoc[tag=exercise]
Note the logging from the `ShoppingCartServiceImpl` in the console that is running `kubectl logs -f`.
:!sectnums:
== Optional commands and events
The commands and events listed in this section are not mandatory for subsequent steps of the tutorial and their details won't be covered on this page. You can implement the commands, events, and `State` management following the pattern we used for the `AddItem` command and `ItemAdded` event in the xref:entity.adoc[previous step]. This is a good exercise to help solidify your knowledge of how to implement `EventSourcedBehavior`. Optional commands and corresponding events that you can add on your own:
* `RemoveItem` - remove an item from the cart
* `AdjustItemQuantity` - adjust the quantity of an item in the cart
* `ItemRemoved`
* `ItemQuantityAdjusted`
After adding the optional commands, you can build and run again and try them out:
Update the quantity of an item:
[source,shell script]
----
grpcurl -d '{"cartId":"cart1", "itemId":"socks", "quantity":6}' -plaintext 127.0.0.1:8101 shoppingcart.ShoppingCartService.UpdateItem
----
Get the cart state again:
[source,shell script]
----
grpcurl -d '{"cartId":"cart1"}' -plaintext 127.0.0.1:8101 shoppingcart.ShoppingCartService.GetCart
----
== Learn more
* xref:concepts:event-sourcing.adoc[Event Sourcing concepts].
* {akka}/typed/persistence.html[Akka Event Sourcing reference documentation {tab-icon}, window="tab"].
* {akka}/typed/cluster-sharding.html[Akka Cluster Sharding reference documentation {tab-icon}, window="tab"].