Make an E-Commerce Application With Akka Serverless

Make an E-Commerce Application With Akka Serverless

November 04, 2021

serverless

Make an E-Commerce Application With Akka Serverless

Introduction

Akka serverless

Akka Serverless is a Platform-as-a-Service that combines API-first, database-less programming and serverless runtime. Developers do not have to set up and tune databases, maintain and provision servers, configure or run compute clusters. The only thing the developer is responsible for is building the serverless service. In contrast, Akka Serverless provides advanced data access patterns like CQRS, Event Sourcing, and CRDTs without the developers learn how to implement these concepts.

Features of Akka Serverless

Database-less

Traditionally, all data, including state is stored in the database. The database needs to be accessed to retrieve even the state, which can contribute significantly to latency. This issue has been solved in Akka serverless by the in-memory state, backed by durable storage, thus reducing latency for data-centric operations and bringing the data to the service when needed. This feature also eliminates the need to set up the database at the backend as Akka serverless handles its nitty-gritty.

API-first

Due to the nature of Akka serverless being database-less, developers need to only think about implementating their APIs, making Akka serverless API-first. It allows the developers to create data objects as required without worrying about how the data needs to be persisted in the durable storage. Another avenue this feature opens up is the level of exposure the developer wants to configure for their services since the platform takes care of the connectivity to the storage.

Serverless

Developers do not need to worry about scaling their application with an increase in demand for their application. Akka serverless handles all the infrastructure required for the services deployed by the developer. The runtime provided by Akka is actor-based, self-healing, and highly scalable distributed runtime.

Akka Serverless Programming Model

Services created by the developer can contain stateless and stateful components. For stateful components, developers can choose the state model and the data management is handled by Akka serverless.

Service

Service is the code written by the developer that specifies the logic of the application being implemented. A service can contain stateless actions, stateful entities, and views.

Actions

Actions contain code that does not require persistence of state. For example, an action can transform an entity upon listening to an event.

Entities

Entities are domain objects that encapsulate data and business logic. Entities can be of different state models, which can be chosen based on the entity’s use case and determines how Akka serverless manages the data.

Views

Views are used to retrieve data from multiple entities. We can use SQL-like queries to retrieve data from the entities.

State model

Traditionally, applications use the database tier to retrieve and store state information. It puts a lot of burden on the developer as it is required to make proper database connections, maintains versions, ensure compatibility between different versions, scale databases, and handle errors. In Akka serverless, a proxy is available at every service. The proxy acts like an in-memory data store that stores all the state information, thus reducing latency from data retrieval operations.

There are two types of state models - the value entity state model and the event-sourced entity state model.

Value Entity state model

Value entity state model only persists the current state into the durable data store. Once an entity with a particular ID is instantiated, its state is cached to make subsequent requests for it quicker. Benefits offered by value entity state model are - scalable - long-lived - addressable behaviours - more similar to traditional databases

Value Entity state model sequence diagram

Event Sourced Entity state model

Event-sourced entity state models capture data changes, instead of overwriting existing values. A log of changes to the data is maintained in a journal. Different services can start reading the journal at different points, resulting in eventual consistency. All parties are guaranteed to catch up at some point even though some may take longer to do so. Benefits offered by event-sourced entity state model - auditing - temporal queries to reconstruct the historical state

Event Sourced Entity state model sequence diagram

Tutorial

The next section of the blog goes into explaining how an application can be designed, created and deployed using Akka serverless. For this example, we will go over how to create an e-commerce application using Akka serverless.

The design of the application is as follows, where the yellow box represents an entity and the blue boxes represent the functions associated with the entity -
Design diagram of cart application

For development ease, we will be using Akka serverless CLI, which can be installed and configured as mentioned here. You will also need to have an Akka serverless account to deploy your application.

Developing an application using Akka serverless entails the following steps -

  • Defining messages
  • Set up services
  • Code the logic
  • Package the services
  • Deploy

Defining messages

The first step involved in creating an application is to define the messages that will be passed around the remote procedure calls. These messages will also reflect on how data will be stored in the database by the Akka serverless proxy. A domain protobuf file is generally required when working with event-sourced entities to persist the data into the journal of events.

cart.proto

syntax = "proto3";

import "akkaserverless/annotations.proto";
import "google/api/annotations.proto";
import "google/protobuf/empty.proto";


// google imports allow to send empty responses and add annotations to expose the service 
// with a REST interface

package com.example.shoppingcart;

message AddLineItem {
    string user_id = 1 [(akkaserverless.field).entity_key = true];
    string product_id = 2;
    string name = 3;
    int32 quantity = 4;
}

message RemoveLineItem {
    string user_id = 1 [(akkaserverless.field).entity_key = true];
    string product_id = 2;
}

message GetShoppingCart {
    string user_id = 1 [(akkaserverless.field).entity_key = true];
}

message LineItem {
    string product_id = 1;
    string name = 2;
    int32 quantity = 3;
}

message Cart {
    repeated LineItem items = 1;
}

domain.proto

syntax = "proto3";

import "akkaserverless/annotations.proto";

package ecommerce.persistence;

// this is an external interface for internal events

option (akkaserverless.file).event_sourced_entity = {
    name: "ShoppingCart"
    entity_type: "cart"
    state: "Cart"
    events: "ItemAdded"
    events: "ItemRemoved"
};

// These messages are internal events
message ItemAdded {
    LineItem item = 1;
}

message ItemRemoved {
    string productId = 1;
}

message Cart {
    repeated LineItem items = 1;
}

message LineItem {
    string productId = 1;
    string name = 2;
    int32 quantity = 3;
}

Set up services

To utilise the message definitions created in the first step, one must declare services. This section contains the functions needed to be defined for the application being developed. Services will contain declarations for remote procedure calls, some of which map to HTTP methods. These RPCs can be commands or events.

cart.proto

service ShoppingCartService {
    option (akkaserverless.service) = {
        type: SERVICE_TYPE_ENTITY
        component: ".domain.ShoppingCart"
    };
    
    // This RPC creates a http endpoint to add an item to a user's cart.
    // AddLineItem is a message defined in the protocol buffer which
    // corresponds to the type of item being passed to the RPC.
    // This function returns an empty message
    rpc AddItem(AddLineItem) returns (google.protobuf.Empty) {
        option (google.api.http) = {
            post: "/cart/{user_id}/items/add"
            body: "*"
        };
    }
    
    // This RPC creates a http endpoint to remove an item to a user's cart.
    // RemoveLineItem is a message defined in the protocol buffer which
    // corresponds to the type of item being passed to the RPC.
    // This function returns an empty message
    rpc RemoveItem(RemoveLineItem) returns (google.protobuf.Empty) {
        option (google.api.http).post = "/cart/{user_id}/items/{product_id}/remove";
    }
    
    // This RPC creates the http endpoint to retrieve all the items from 
    // a user's cart.
    // GetShoppingCart is a message defined in the protocol buffer which
    // corresponds to the type of function being passed to the RPC
    // This function returns a message of the type Cart which is also
    // defined in the protocol buffer.
    rpc GetCart(GetShoppingCart) returns (Cart) {
        option (google.api.http) = {
            get: "/carts/{user_id}"
            additional_bindings: {
                get: "/carts/{user_id}/items"
                response_body: "items"
            }
        };
    }
}

Code the logic

Entities will contain the state data. The entities will be created in the javascript files and they will be of a type defined as a message in the protobuf files. The created entities are also to be initialised.

Behaviour includes command and event handlers. The commands and events correspond to the RPCs and events in the service and message definitions respectively.

The following node packages need to be installed and saved to package.json as dependencies -

cart.js

import as from '@lightbend/akkaserverless-javascript-sdk';
const EventSourcedEntity = as.EventSourcedEntity;

const entity = new EventSourcedEntity(
    ['cart.proto', 'domain.proto'],
    "com.example.shoppingcart.ShoppingCartService",
    'cart',
    {
        snapshotEvery: 100,
        includeDirs: ['./'],
        serializeAllowPrimitives: true,
        serializeFallbackToJson: true
    }
);

// Map the types of messages defined in domain.proto to be able to persist 
// external events to internal events
const pkg = "ecommerce.persistence.";
const ItemAdded = entity.lookupType(pkg + 'ItemAdded');
const ItemRemoved = entity.lookupType(pkg + 'ItemRemoved');
const Cart = entity.lookupType(pkg + 'Cart');

entity.setInitial(userId => Cart.create({ items: [] }));

// Set behaviour RPCs(commands) to corresponding functions that define the 
// behaviour 
entity.setBehavior(cart => {
    return {
        commandHandlers: {
            AddItem: addItem,
            RemoveItem: removeItem,
            GetCart: getCart
        },
        eventHandlers: {
            ItemAdded: itemAdded,
            ItemRemoved: itemRemoved
        }
    };
});

// Define the functions that handle the behaviour
function addItem(item, cart, ctx) {
    if (item.quantity < 1) {
        ctx.fail('Cannot add negative quantity to item ' + item.productId);
    } else {
        const itemAdded = ItemAdded.create({
            item: {
                productId: item.productId,
                name: item.name,
                quantity: item.quantity
            }
        });
        ctx.emit(itemAdded);
        return {};
    }
}

function removeItem(item, cart, ctx) {
    const existing = cart.items.find(existingItem => {
        return existingItem.productId === item.productId;
    });

    if (!existing) {
        ctx.fail('Item ' + item.productId + ' not in cart');
    } else {
        const itemRemoved = ItemRemoved.create({
            productId: item.productId
        });
        ctx.emit(itemRemoved);
        return {};
    }
}

function getCart(request, cart) {
    return cart;
}

// Persisting external events using internal events.
function itemAdded(added, cart) {
    const existing = cart.items.find(item => {
        return item.productId === added.item.productId;
    });
    
    if (existing) {
        existing.quantity = existing.quantity + added.item.quantity;
    } else {
        cart.items.push(added.item);
    }

    return cart;
}

function itemRemoved(removed, cart) {
    cart.items = cart.items.filter(item => {
        return item.productId !== removed.productId;
    });

    return cart;
}

export default entity;

Package the services

The created services must be packaged into a container image to be able to deploy onto Akka serverless. Once the image is built, it must be pushed to a public container registry or a private registry that has provided required permissions if the container registry requires authentication.

The following dockerfile sufficiently builds the services into a container image which can be pushed to a container registry.

FROM node:12.18.0-buster AS builder
WORKDIR /home/node
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
EXPOSE 8080
CMD ["npm", "start"]

Deploy

The packaged services can be deployed to Akka serverless using the following command -

demo@akka:~$ akkasls service deploy cart-app \
                container-uri/container-name:tags

To expose the deployed services to HTTP, the following command can be used -

demo@akka:~$ akkasls service expose cart-app \
                --enable-cors \
                --project project-name

Upon deployment and exposing the services, the HTTP endpoints can be accessed using cURL. The endpoints can also be accessed via Javascript APIs like fetch, AJAX or jQuery. You may also use gRPC clients for the language being used for the component communicating with the backend of your application to send requests to the application deployed on Akka serverless.

Demo

The demo contains additional components for users and admin functionality. Please refer to the repository available at this link.

Demo video -

Useful Links

Useful resources to get started with Akka serverless are available and documented at this link.

Antstack Blog Post
Antstack Blog Post

Are you planning to go to serverless? AntStack is a cloud computing service and consulting company primarily focusing on Serverless Computing. We help companies get up and running with serverless, and we’ll make sure that there are no limits.

Keep track of our socials and connect with us - LinkedIn