Illustration Image

Cassandra.Link

The best knowledge base on Apache Cassandra®

Helping platform leaders, architects, engineers, and operators build scalable real time data platforms.

8/5/2020

Reading time:5 min

calvinlfer/es-cqrs-shopping-cart

by John Doe

A application that uses Event Sourcing (ES) and Command Query Responsibility segregation (CQRS) to implement a shoppingcart and provides a way to perform analytics. The command side is designed to provide shopping cart functionality tomembers and the different query sides are designed to provide analytics on member's shopping carts. Please note thatthe query nodes are not the views (UI) themselves but rather the components that populate the Query side's databasesthat the views would use to display data to the user.Running the applicationStart up dependencies (Cassandra and ZooKeeper) with Docker Compose using docker-compose upIf you want to run any SQL based Query nodes, also start up PostgreSQL usingdocker-compose -f pg-docker-compose.yaml upStart applicationCommand nodes require the following environment variables to be specifiedHOST_IP: IP of the host machine (e.g. 192.168.1.144 or 127.0.0.1)HOST_PORT: Remoting port (e.g. 2552)MANAGEMENT_PORT: HTTP port that exposes cluster management (e.g. 19999)REST_HOST: IP of the host machine (e.g. localhost, 127.0.0.1, etc.) to expose the REST APIREST_PORT: Port to expose the REST API (e.g. 9001)The Query node for Vendor Billing requires the following environment variables to be specifiedHOST_IP: IP of the host machine (e.g. 192.168.1.144 or 127.0.0.1)HOST_PORT: Remoting port (e.g. 2552)The Query node for Popular Items requires the following environment variables to be specifiedHOST_IP: IP of the host machine (e.g. 192.168.1.144 or 127.0.0.1)HOST_PORT: Remoting port (e.g. 2552)Note: Make sure that any ports do not conflict between any nodes in the cluster if you plan to run them on the samemachineCommand nodesThe command nodes are responsible for providing the operational functionality of the shopping cart. This modules allowsyou to store items in a shopping cart and check out when you are done. If you go away and come back later, it willremember exactly what you have purchased thanks to Akka Persistence. This component is able to scale horizontally thanksto Akka Cluster Sharding. You can interact with this component in two ways:REST APIIn order to place an item in the shopping cart (Shopping Cart: 9a475f59-8863-43cc-aebd-7da999c16bea):POST http://localhost:9001/cart/9a475f59-8863-43cc-aebd-7da999c16bea{ "productId": "9054a277-9998-4bb4-be89-7d1ac45828d2", "vendorId": "fbea6379-b76c-478b-8f86-4f1626fb8acf", "name": "awesome-desktop-pc", "price": "3200", "quantity": 1}Removing an item (by Product ID: 9054a277-9998-4bb4-be89-7d1ac45828d2) from the shopping cart:DELETE http://localhost:9001/cart/9a475f59-8863-43cc-aebd-7da999c16bea/productId/9054a277-9998-4bb4-be89-7d1ac45828d2Getting the contents of your shopping cart:GET http://localhost:9001/cart/9a475f59-8863-43cc-aebd-7da999c16beaIn order to checkout with the items you have in your shopping cart:POST http://localhost:9001/cart/9a475f59-8863-43cc-aebd-7da999c16bea/checkoutThis will clear your shopping cart.Command line interfaceInitially when this application was being created I wanted a quick way to try things out so I came up with a reallysimple way to communicate with the system to try things out. It will take care of generating UUIDs for the productsbased on the names that you use. You can type the following commands into the application:Choose a shopping cart for a person:change-member calvinAdd an item to the existing shopping cart you have selected:add orangeRemove an item to the existing shopping cart you have selected:remove orangeAdjust the quantity of an existing item (you can use negative numbers to decrease) in your shopping cart you haveselected:adjust orange 10Checkout all the items in your existing cart:checkoutProvide current information about the shopping cart (UUID):current-memberProvide information about available commands:Query nodesThere are a variety of query components. The purpose of each query component is to demonstrate how to populate thedatabase of a read-side view but not actually provide the UI functionality of the view. In a sense Query nodes are morelike hydrators as they provide data for the view side UI to consume and display in a way they see fit. There are threequeries that consume from the event journal directly and write to the read-side view in an exactly-once manner providingtransactional guarantees (popular-items, vendor-billing, vendor-billing-jdbc). popular-items and vendor-billingconsume from the journal and hydrate Cassandra tables whilst vendor-billing-jdbc hydrates a PostgreSQL table. Lastbut not least is the item-purchased-events hydrator which is responsible consuming purchased items from the journaland publishing those events to Kafka in an at-least-once fashion. The modules that consume data from the event journaland publish data to the read-side database directly make use of a offset tracking table where they record their progressand update the data in a transactional manner. All query/hydrator components make use of this offset tracking tablebut the item-purchased-events module cannot perform transactional writes since it updates two different systems(Cassandra for offset-tracking and Kafka for event publishing). Each query module is run as a Cluster Singleton andjoins the same cluster as the command nodes in order to make use of some optimizations under the hood. You can runmultiple query nodes (of the same type) at the same time but they will operate in a active-passive manner and hand-offwill occur when the active query node goes down.We'll now examine each query/hydrator module:popular-itemsThis module is responsible for tallying up the most popular items that were purchased for each day. It pulls eventsfrom the event journal via Akka Persistence query and writes them to a Cassandra table.CREATE TABLE item_quantity_by_day ( vendorid uuid, productid uuid, year int, month int, day int, quantity int, name string PRIMARY KEY((vendorid, productid, year, month), day)) WITH CLUSTERING ORDER BY (day ASC);vendor-billingThis module is responsible for tallying up the most popular items that were purchased for each day. It pulls eventsfrom the event journal via Akka Persistence query and writes them to a Cassandra table.CREATE TABLE balance_by_vendor ( vendorId uuid, year int, month int, balance decimal, PRIMARY KEY ((vendorId, year), month)) WITH CLUSTERING ORDER BY (month DESC)vendor-billing-jdbcPerforms the same function as vendor-billing except it writes to PostgreSQL instead of Cassandra.CREATE TABLE vendor_billing( vendor_id UUID NOT NULL, year INTEGER NOT NULL, month INTEGER NOT NULL, balance NUMERIC(21, 2) NOT NULL, CONSTRAINT "vendorId_year_month_pk" PRIMARY KEY (vendor_id, year, month));item-purchased-eventsThis module is responsible for pulling all item-purchased events from the event journal and pushing them to a Kafkatopic for consumption by further downstream services. The updates to Kafka have an at-least-once delivery guarantee soduplicates can occur because we cannot guarantee transactions can happen as we use Cassandra to track journal offsetsand we publish data to Kafka separately.ArchitectureWIP

Illustration Image

A application that uses Event Sourcing (ES) and Command Query Responsibility segregation (CQRS) to implement a shopping cart and provides a way to perform analytics. The command side is designed to provide shopping cart functionality to members and the different query sides are designed to provide analytics on member's shopping carts. Please note that the query nodes are not the views (UI) themselves but rather the components that populate the Query side's databases that the views would use to display data to the user.

Running the application

  • Start up dependencies (Cassandra and ZooKeeper) with Docker Compose using docker-compose up

  • If you want to run any SQL based Query nodes, also start up PostgreSQL using docker-compose -f pg-docker-compose.yaml up

  • Start application

    • Command nodes require the following environment variables to be specified

      • HOST_IP: IP of the host machine (e.g. 192.168.1.144 or 127.0.0.1)
      • HOST_PORT: Remoting port (e.g. 2552)
      • MANAGEMENT_PORT: HTTP port that exposes cluster management (e.g. 19999)
      • REST_HOST: IP of the host machine (e.g. localhost, 127.0.0.1, etc.) to expose the REST API
      • REST_PORT: Port to expose the REST API (e.g. 9001)
    • The Query node for Vendor Billing requires the following environment variables to be specified

      • HOST_IP: IP of the host machine (e.g. 192.168.1.144 or 127.0.0.1)
      • HOST_PORT: Remoting port (e.g. 2552)
    • The Query node for Popular Items requires the following environment variables to be specified

      • HOST_IP: IP of the host machine (e.g. 192.168.1.144 or 127.0.0.1)
      • HOST_PORT: Remoting port (e.g. 2552)

Note: Make sure that any ports do not conflict between any nodes in the cluster if you plan to run them on the same machine

Command nodes

The command nodes are responsible for providing the operational functionality of the shopping cart. This modules allows you to store items in a shopping cart and check out when you are done. If you go away and come back later, it will remember exactly what you have purchased thanks to Akka Persistence. This component is able to scale horizontally thanks to Akka Cluster Sharding. You can interact with this component in two ways:

REST API

In order to place an item in the shopping cart (Shopping Cart: 9a475f59-8863-43cc-aebd-7da999c16bea):

POST http://localhost:9001/cart/9a475f59-8863-43cc-aebd-7da999c16bea

{
	"productId": "9054a277-9998-4bb4-be89-7d1ac45828d2",
	"vendorId": "fbea6379-b76c-478b-8f86-4f1626fb8acf",
	"name": "awesome-desktop-pc",
	"price": "3200",
	"quantity": 1
}

Removing an item (by Product ID: 9054a277-9998-4bb4-be89-7d1ac45828d2) from the shopping cart:

DELETE http://localhost:9001/cart/9a475f59-8863-43cc-aebd-7da999c16bea/productId/9054a277-9998-4bb4-be89-7d1ac45828d2

Getting the contents of your shopping cart:

GET http://localhost:9001/cart/9a475f59-8863-43cc-aebd-7da999c16bea

In order to checkout with the items you have in your shopping cart:

POST http://localhost:9001/cart/9a475f59-8863-43cc-aebd-7da999c16bea/checkout

This will clear your shopping cart.

Command line interface

Initially when this application was being created I wanted a quick way to try things out so I came up with a really simple way to communicate with the system to try things out. It will take care of generating UUIDs for the products based on the names that you use. You can type the following commands into the application:

Choose a shopping cart for a person:

change-member calvin

Add an item to the existing shopping cart you have selected:

add orange

Remove an item to the existing shopping cart you have selected:

remove orange

Adjust the quantity of an existing item (you can use negative numbers to decrease) in your shopping cart you have selected:

adjust orange 10

Checkout all the items in your existing cart:

checkout

Provide current information about the shopping cart (UUID):

current-member

Provide information about available commands:

Query nodes

There are a variety of query components. The purpose of each query component is to demonstrate how to populate the database of a read-side view but not actually provide the UI functionality of the view. In a sense Query nodes are more like hydrators as they provide data for the view side UI to consume and display in a way they see fit. There are three queries that consume from the event journal directly and write to the read-side view in an exactly-once manner providing transactional guarantees (popular-items, vendor-billing, vendor-billing-jdbc). popular-items and vendor-billing consume from the journal and hydrate Cassandra tables whilst vendor-billing-jdbc hydrates a PostgreSQL table. Last but not least is the item-purchased-events hydrator which is responsible consuming purchased items from the journal and publishing those events to Kafka in an at-least-once fashion. The modules that consume data from the event journal and publish data to the read-side database directly make use of a offset tracking table where they record their progress and update the data in a transactional manner. All query/hydrator components make use of this offset tracking table but the item-purchased-events module cannot perform transactional writes since it updates two different systems (Cassandra for offset-tracking and Kafka for event publishing). Each query module is run as a Cluster Singleton and joins the same cluster as the command nodes in order to make use of some optimizations under the hood. You can run multiple query nodes (of the same type) at the same time but they will operate in a active-passive manner and hand-off will occur when the active query node goes down.

We'll now examine each query/hydrator module:

popular-items

This module is responsible for tallying up the most popular items that were purchased for each day. It pulls events from the event journal via Akka Persistence query and writes them to a Cassandra table.

CREATE TABLE item_quantity_by_day (
  vendorid uuid,
  productid uuid,
  year int,
  month int,
  day int,
  quantity int,
  name string
  PRIMARY KEY((vendorid, productid, year, month), day)
) WITH CLUSTERING ORDER BY (day ASC);

vendor-billing

This module is responsible for tallying up the most popular items that were purchased for each day. It pulls events from the event journal via Akka Persistence query and writes them to a Cassandra table.

CREATE TABLE balance_by_vendor (
  vendorId uuid,
  year int,
  month int,
  balance decimal,
  PRIMARY KEY ((vendorId, year), month)
) WITH CLUSTERING ORDER BY (month DESC)

vendor-billing-jdbc

Performs the same function as vendor-billing except it writes to PostgreSQL instead of Cassandra.

CREATE TABLE vendor_billing
(
  vendor_id UUID           NOT NULL,
  year      INTEGER        NOT NULL,
  month     INTEGER        NOT NULL,
  balance   NUMERIC(21, 2) NOT NULL,
  CONSTRAINT "vendorId_year_month_pk"
  PRIMARY KEY (vendor_id, year, month)
);

item-purchased-events

This module is responsible for pulling all item-purchased events from the event journal and pushing them to a Kafka topic for consumption by further downstream services. The updates to Kafka have an at-least-once delivery guarantee so duplicates can occur because we cannot guarantee transactions can happen as we use Cassandra to track journal offsets and we publish data to Kafka separately.

Architecture

WIP

Related Articles

analytics
streaming
visualization

Keen - Event Streaming Platform

John Doe

2/3/2024

Checkout Planet Cassandra

Claim Your Free Planet Cassandra Contributor T-shirt!

Make your contribution and score a FREE Planet Cassandra Contributor T-Shirt! 
We value our incredible Cassandra community, and we want to express our gratitude by sending an exclusive Planet Cassandra Contributor T-Shirt you can wear with pride.

Join Our Newsletter!

Sign up below to receive email updates and see what's going on with our company

Explore Related Topics

AllKafkaSparkScyllaSStableKubernetesApiGithubGraphQl

Explore Further

cassandra