Build your first application

This guide will walk you through the process of setting up your environment, generating a project, and implementing a simple application. By the end, you will have a functional Shopping Cart application built with the Akka SDK.

Overview

The Akka SDK comes with Maven support that enables you to get started quickly. In this guide, you will:

  • Use the Akka Maven archetype to generate a skeleton project that follows the recommended onion architecture.

  • Understand how to use the Akka Maven parent POM to define dependencies and run your application locally.

Prerequisites

Generate and build the skeleton project

The Maven archetype template prompts you to specify the project’s group ID, name and version interactively. Run it using the commands shown for your OS.

In IntelliJ, you can skip the command line. Open the IDE, select File > New > Project, and click to activate Create from archetype. Use the UI to locate the archetype and fill in the blanks.

Follow these steps to generate and build your project:

  1. From a command line, run the template in a convenient location:

    Linux or macOS
    mvn archetype:generate \
      -DarchetypeGroupId=io.akka \
      -DarchetypeArtifactId=akka-javasdk-archetype \
      -DarchetypeVersion=3.0.0
    Windows 10+
    mvn archetype:generate ^
      -DarchetypeGroupId=io.akka ^
      -DarchetypeArtifactId=akka-javasdk-archetype ^
      -DarchetypeVersion=3.0.0
  2. Fill in

    • groupId: com.example

    • artifactId: shoppingcart

    • version: 1.0-SNAPSHOT

    • package: shoppingcart

  3. Navigate to the new project directory.

  4. Open it in your preferred IDE / Editor.

Implementing first component

Akka has different kinds of components that you can use to implement your application. The list of components are:

For this Getting started walk-through, we will implement a Shopping Cart based on an Event Sourced Entity.

Through our "Shopping Cart" Event Sourced Entity we expect to manage our cart, adding and removing items as we please. Being event-sourced means it will represent changes to state as a series of domain events. Let’s have a look at what kind of model we expect to store and the events our entity might generate.

Define the domain model

First, define the domain class ShoppingCart in package shoppingcart.domain. This class should be located in the src/main/java/shoppingcart/domain/ directory and saved as ShoppingCart.java.

package shoppingcart.domain;

import java.util.List;

public record ShoppingCart(String cartId, List<LineItem> items, boolean checkedOut) { (1)

  public record LineItem(String productId, String name, int quantity) { (2)
    public LineItem withQuantity(int quantity) {
      return new LineItem(productId, name, quantity);
    }
  }

}
1 Our ShoppingCart is fairly simple, being composed only by a cartId and a list of line items.
2 A LineItem represents a single product and the quantity we intend to buy.

Next, let’s define a domain event for adding items to the cart. Add an interface ShoppingCartEvent with the ItemAdded domain event in package shoppingcart.domain. This class should be located in the src/main/java/shoppingcart/domain/ directory and saved as ShoppingCartEvent.java:

package shoppingcart.domain;

import akka.javasdk.annotations.TypeName;

public sealed interface ShoppingCartEvent { (1)

  @TypeName("item-added") (2)
  record ItemAdded(ShoppingCart.LineItem item) implements ShoppingCartEvent {
  }

}
1 ItemAdded event derives from the ShoppingCartEvent interface.
2 Specifying a logical type name, required for serialization.

Jumping back to the ShoppingCart domain class, add business logic to handle the ItemAdded domain event for adding items to the cart:

public ShoppingCart onItemAdded(ShoppingCartEvent.ItemAdded itemAdded) {
  var item = itemAdded.item();
  var lineItem = updateItem(item, this); (1)
  List<LineItem> lineItems = removeItemByProductId(this, item.productId()); (2)
  lineItems.add(lineItem); (3)
  lineItems.sort(Comparator.comparing(LineItem::productId));
  return new ShoppingCart(cartId, lineItems, checkedOut); (4)
}

private static LineItem updateItem(LineItem item, ShoppingCart cart) {
  return cart.findItemByProductId(item.productId())
    .map(li -> li.withQuantity(li.quantity() + item.quantity()))
    .orElse(item);
}

private static List<LineItem> removeItemByProductId(ShoppingCart cart, String productId) {
  return cart.items().stream()
    .filter(lineItem -> !lineItem.productId().equals(productId))
    .collect(Collectors.toList());
}

public Optional<LineItem> findItemByProductId(String productId) {
  Predicate<LineItem> lineItemExists =
      lineItem -> lineItem.productId().equals(productId);
  return items.stream().filter(lineItemExists).findFirst();
}
1 For an existing item, we will make sure to sum the existing quantity with the incoming one.
2 Returns an update list of items without the existing item.
3 Adds the update item to the shopping cart.
4 Returns a new instance of the shopping cart with the updated line items.

If your integrated development environment (IDE) did not detect it, you may need to add the following imports manually:

import java.util.stream.Collectors;
import java.util.Comparator;
import java.util.Optional;
import java.util.function.Predicate;

Define the entity

Create an Event Sourced Entity named ShoppingCartEntity in package shoppingcart.application. This class should be located in the src/main/java/shoppingcart/application/ directory and saved as ShoppingCartEntity.java. The class signature should look like this:

package shoppingcart.application;

import akka.Done;
import akka.javasdk.annotations.ComponentId;
import akka.javasdk.eventsourcedentity.EventSourcedEntity;
import akka.javasdk.eventsourcedentity.EventSourcedEntityContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import shoppingcart.domain.ShoppingCart;
import shoppingcart.domain.ShoppingCart.LineItem;
import shoppingcart.domain.ShoppingCartEvent;

import java.util.Collections;

@ComponentId("shopping-cart") (2)
public class ShoppingCartEntity extends EventSourcedEntity<ShoppingCart, ShoppingCartEvent> { (1)
}
1 Extend EventSourcedEntity<ShoppingCart, ShoppingCartEvent>, where ShoppingCart is the type of state this entity will store, and ShoppingCartEvent is the interface for the events it persists.
2 Annotate the class so Akka can identify it as an Event Sourced Entity.

At this point, you may encounter an error stating: The type ShoppingCartEntity must implement the inherited abstract method EventSourcedEntity<ShoppingCart,ShoppingCartEvent>.applyEvent(ShoppingCartEvent). This error is expected, and we will implement this abstract method in the next step.

Inside ShoppingCartEntity, define an addItem command handler to generate an ItemAdded event, and an event handler to process the event:

  public Effect<Done> addItem(LineItem item) {
    if (currentState().checkedOut()) {
      logger.info("Cart id={} is already checked out.", entityId);
      return effects().error("Cart is already checked out.");
    }
    if (item.quantity() <= 0) { (1)
      logger.info("Quantity for item {} must be greater than zero.", item.productId());
      return effects().error("Quantity for item " + item.productId() + " must be greater than zero.");
    }

    var event = new ShoppingCartEvent.ItemAdded(item); (2)

    return effects()
        .persist(event) (3)
        .thenReply(newState -> Done.getInstance()); (4)
  }

  @Override
  public ShoppingCart applyEvent(ShoppingCartEvent event) {
    return switch (event) {
      case ShoppingCartEvent.ItemAdded evt -> currentState().onItemAdded(evt); (5)
    };
  }
1 Validate the quantity of items added is greater than zero.
2 Create a new ItemAdded event representing the change to the state of the cart.
3 Persist the event by returning an Effect with effects().persist.
4 Acknowledge that the event was successfully persisted.
5 Event handler to process the ItemAdded event and return the updated state.

Inside ShoppingCartEntity, define a getCart command handler to retrieve the state of the shopping cart:

private final String entityId;

private static final Logger logger = LoggerFactory.getLogger(ShoppingCartEntity.class);

public ShoppingCartEntity(EventSourcedEntityContext context) {
  this.entityId = context.entityId(); (1)
}

@Override
public ShoppingCart emptyState() { (2)
  return new ShoppingCart(entityId, Collections.emptyList(), false);
}

public ReadOnlyEffect<ShoppingCart> getCart() {
  return effects().reply(currentState()); (3)
}
1 Store the entityId in an internal attribute, to be used elsewhere.
2 Define the initial state.
3 Return the current state as a reply to the request.

Define the external API

The shopping cart API is defined by the ShoppingCartEndpoint.

Create a class named ShoppingCartEndpoint in package shoppingcart.api. This class should be located in the src/main/java/shoppingcart/api/ directory and saved as ShoppingCartEndpoint.java:

package shoppingcart.api;

import akka.http.javadsl.model.HttpResponse;
import akka.javasdk.annotations.Acl;
import akka.javasdk.annotations.http.HttpEndpoint;
import akka.javasdk.annotations.http.Get;
import akka.javasdk.annotations.http.Put;
import akka.javasdk.client.ComponentClient;
import akka.javasdk.http.HttpResponses;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import shoppingcart.application.ShoppingCartEntity;
import shoppingcart.domain.ShoppingCart;

import java.util.concurrent.CompletionStage;


// Opened up for access from the public internet to make the sample service easy to try out.
// For actual services meant for production this must be carefully considered, and often set more limited
@Acl(allow = @Acl.Matcher(principal = Acl.Principal.INTERNET))
@HttpEndpoint("/carts") (1)
public class ShoppingCartEndpoint {

  private final ComponentClient componentClient;

  private static final Logger logger = LoggerFactory.getLogger(ShoppingCartEndpoint.class);

  public ShoppingCartEndpoint(ComponentClient componentClient) { (2)
    this.componentClient = componentClient;
  }

  @Get("/{cartId}") (3)
  public CompletionStage<ShoppingCart> get(String cartId) {
    logger.info("Get cart id={}", cartId);
    return componentClient.forEventSourcedEntity(cartId) (4)
        .method(ShoppingCartEntity::getCart)
        .invokeAsync(); (5)
  }

  @Put("/{cartId}/item") (6)
  public CompletionStage<HttpResponse> addItem(String cartId, ShoppingCart.LineItem item) {
    logger.info("Adding item to cart id={} item={}", cartId, item);
    return componentClient.forEventSourcedEntity(cartId)
      .method(ShoppingCartEntity::addItem)
      .invokeAsync(item)
      .thenApply(__ -> HttpResponses.ok()); (7)
  }

}
1 Annotate the class so Akka can identify it as an Endpoint with a common path prefix for all methods /carts.
2 ComponentClient utility enables components to interact with each other.
3 GET endpoint path is combined with a path parameter name, e.g. /carts/123.
4 ComponentClient calling a command handler on an Event Sourced Entity from inside an Endpoint.
5 Result of request is a CompletionStage<T>, in this case a CompletionStage<ShoppingCart>.
6 Use path parameter {cartId} in combination with request body ShoppingCart.LineItem.
7 Map request to a more suitable response, in this case an HTTP 200 OK response.

Run locally

Start your service locally:

mvn compile exec:java

Once successfully started, any defined endpoints become available at localhost:9000.

Let’s send some requests using curl.

Add some T-shirts to a shopping cart:

curl -i -XPUT -H "Content-Type: application/json" localhost:9000/carts/123/item -d '
{"productId":"akka-tshirt", "name":"Akka Tshirt", "quantity": 3}'

Add some blue jeans to the shopping cart:

curl -i -XPUT -H "Content-Type: application/json" localhost:9000/carts/123/item -d '
{"productId":"blue-jeans", "name":"Blue Jeans", "quantity": 2}'

Add a few more T-shirts to the shopping cart:

curl -i -XPUT -H "Content-Type: application/json" localhost:9000/carts/123/item -d '
{"productId":"akka-tshirt", "name":"Akka Tshirt", "quantity": 3}'

Request to see all of the items in the cart:

curl localhost:9000/carts/123

Observe all of the items in the cart:

{"cartId":"123","items":[{"productId":"akka-tshirt","name":"Akka Tshirt","quantity":6},
{"productId":"blue-jeans","name":"Blue Jeans","quantity":5}],"checkedOut":false}

Explore the local console

To get a clear view of your locally running service, install the Akka CLI. It provides a local web-based management console.

After you have installed the CLI, start the local console:

akka local console

This will start a Docker container running the local console:

> Local Console is running at:             http://localhost:3000
 - shopping-cart-quickstart is running at: localhost:9000
--------------------

You can open http://localhost:3000/ to see your local service in action:

local console first app events

Here, you can view not only the current state of the cart, but also a detailed log of events, and the corresponding state changes that occurred along the way.

Deploy to akka.io

  1. If you have not already done so, install the Akka CLI.

  2. Authenticate the CLI with your Akka account:

akka auth login
  1. Create a project:

akka projects new my-first-app --region aws-us-east-2

Observe that your project was created successfully:

NAME          DESCRIPTION   ID                                     OWNER                   REGION
my-first-app                703391f5-cee4-4d50-8a7f-07c5d1183aaa   your-organization-name  aws-us-east-2

To make this the currently active project, run: 'akka config set project my-first-app'
  1. Make the project you just created the currently active project:

akka config set project my-first-app
  1. Build a container image of your shopping cart service:

mvn clean install -DskipTests
  1. Take note of the container name and tag from last line in the output, for example:

DOCKER> Tagging image shoppingcart:1.0-SNAPSHOT-20241028102843 successful!
  1. Deploy your service, replacing:

    • container-name with the container name from the mvn install output in the previous step

    • tag-name with the tag name from the mvn install output in the previous step

akka service deploy cart-service shoppingcart:tag-name --push

Your service named cart-service will now begin deploying.

  1. Verify the deployment status of your service:

    akka service list

    A service status can be one of the following:

    • Ready: All service instances are up-to-date and fully available.

    • UpdateInProgress: Service is updating.

    • Unavailable: No service instances are available.

    • PartiallyReady: Some, but not all, service instances are available.

Approximately one minute after deploying, your service status should become Ready.

  1. Expose your service to the internet:

$ akka service expose cart-service
Service 'cart-service' was successfully exposed at: spring-tooth-3406.us-east1.akka.app

Congratulations, you have successfully deployed your service. You can now access it using the hostname described in the output of the command above.

Invoke your deployed service

You can use cURL to invoke your service, replacing URL with the hostname that it was exposed as.

Add an item to the shopping cart:

curl -i -XPUT -H "Content-Type: application/json" https://spring-tooth-3406.us-east1.akka.app/carts/123/item -d '
{"productId":"akka-tshirt", "name":"Akka Tshirt", "quantity": 10}'

Get cart state:

curl https://spring-tooth-3406.us-east1.akka.app/carts/123

Explore the console

  1. Open the Akka Console.

  2. Navigate to the Project where the Service is deployed.

  3. Click on the Service card of the Service, it shows detailed information about the running service.

console first app service

Download full sample

You can download the full source code of the Shopping Cart sample as a zip file.

Next steps

Now that you’ve built and deployed a shopping cart service, take your Akka skills to the next level:

  1. Expand the service: Explore other Akka components to enhance your application with additional features.

  2. Explore other Akka samples: Discover more about Akka by exploring different use cases for inspiration.

  3. Join the community: Visit the Support page to find resources where you can connect with other Akka developers and expand your knowledge.