calvinlfer / es-cqrs-shopping-cart

A resilient and scalable shopping cart system designed using Event Sourcing (ES) and Command Query Responsibility Segregation (CQRS)

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Shopping Cart

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:

help

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

About

A resilient and scalable shopping cart system designed using Event Sourcing (ES) and Command Query Responsibility Segregation (CQRS)


Languages

Language:Scala 97.8%Language:Groovy 2.2%