Create proto files

Akka Serverless services use gRPC protocol buffers (protobuf) for serialization. Files with a .proto extension define the messages, data types, and service endpoints. The compiler uses these definitions to generate service and client interfaces. For the shoppingcart tutorial, you will create two .proto files:

  • cart.proto for the external interface (the API) and,

  • domain.proto for internal state model.

Defining the the external and internal interfaces in different .proto files allows you to evolve the data model separately from the public API.

1. Create the external interface

The external interface exposes the service to other services in your project and to external clients.

  1. Create the cart.proto file in the shoppingcart directory and open it for editing.

  2. Add the syntax version, a package name, and import the Google and Akka Serverless definitions:

    syntax = "proto3";
    
    package com.example.shoppingcart; (1)
    
    import "akkaserverless/annotations.proto"; (2)
    import "google/api/annotations.proto"; (3)
    import "google/protobuf/empty.proto";
    1 The package declaration prevents naming conflicts.
    2 The akkaserverless imports add event sourcing and key capabilities.
    3 The google imports allow empty responses and add annotations to expose the service with a REST interface.

1.1. Add message definitions

The cart.proto file defines messages that can be received by your service as requests and are sent back as responses. Message definitions contain a set of typed fields. Each entity requires a key, which is defined by the annotation [(.akkaserverless.field.entity_key) = true]. The key identifies which instance of an entity should receive the message.

For the shopping cart, you need the ability to add and remove items from the cart, and get a particular user’s cart.

  1. Add the shoppingcart message definitions for that functionality:

    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;
    }
In the full listing below, comments explain the messages.

1.2. Add the service

To use the messages with the entity implementation, add a ShoppingCart service, which will define the remote procedure and HTTP calls for the messages.

  1. Add the ShoppingCartService service:

    service CartService {
        option (akkaserverless.service) = {
          type: SERVICE_TYPE_ENTITY
          component: ".domain.ShoppingCart"
        };
  2. In the ShoppingCartService, add a remote procedure call for AddItem:

    rpc AddItem(AddLineItem) returns (google.protobuf.Empty) {
            option (google.api.http) = {
                post: "/cart/{user_id}/items/add"
                body: "*"
            };
        }
    The AddItem remote procedure call requires an AddLineItem message and responds with an empty message. The google.api.http option instructs Akka Serverless to expose this remote procedure call as a POST HTTP call with the above path.
  3. In the ShoppingCartService, add a remote procedure call for RemoveItem:

    rpc RemoveItem(RemoveLineItem) returns (google.protobuf.Empty) {
            option (google.api.http).post = "/cart/{user_id}/items/{product_id}/remove";
        }
  4. In the ShoppingCartService, add a remote procedure call for GetCart:

    rpc GetCart(GetShoppingCart) returns (Cart) {
            option (google.api.http) = {
                get: "/carts/{user_id}"
                additional_bindings: {
                    get: "/carts/{user_id}/items"
                    response_body: "items"
                }
            };
        }
    }

The completed cart.proto file

The completed cart.proto example:

/**
 * Copyright 2021 Lightbend Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * This file contains the public interface, the API, of the shopping cart.
 * As a best practice, the external interface and the internal events should
 * be in different files
 */

/**
 * Specify the version of the Protobuf syntax
 */
syntax = "proto3";

/**
 * Packages prevent name clashes between protocol messages
 * In this case, the com.example.shoppingcart package is used for all services in this
 * repository and the persistence package is used for all domain objects
 */
package com.example.shoppingcart;

/**
 * Imports allow you to use definitions from other protobuf files. In
 * this case:
 * akkaserverless/annotations.proto contains definitions
 * to work with Akka Serverless.
 * google/api/annotations.proto contains definitions to add
 * HTTP based endpoints
 * google/protobuf/empty.proto contains definitions to send back
 * empty responses
 */
import "akkaserverless/annotations.proto";
import "google/api/annotations.proto";
import "google/protobuf/empty.proto";

/**
 * The AddLineItem is a request message to add an item to
 * a shopping cart (where no response message is sent back).
 * The entity key is used by Akka Serverless to find the correct entity
 */
message AddLineItem {
  string user_id = 1 [(akkaserverless.field).entity_key = true];
  string product_id = 2;
  string name = 3;
  int32 quantity = 4;
}

/**
 * The RemoveLineItem is a request message to remove an item from
 * a shopping cart (where no response message is sent back).
 * The entity key is used by Akka Serverless to find the correct entity
 */
message RemoveLineItem {
  string user_id = 1 [(akkaserverless.field).entity_key = true];
  string product_id = 2;
}

/**
 * The GetShoppingCart is a request message to get a shopping cart
 * for a specific customer (where cart is the response message).
 * The entity key is used by Akka Serverless to find the correct entity
 */
message GetShoppingCart {
  string user_id = 1 [(akkaserverless.field).entity_key = true];
}

/**
 * The LineItem message is used in the Cart message to represent
 * different items in the cart
 */
message LineItem {
  string product_id = 1;
  string name = 2;
  int32 quantity = 3;
}

/**
 * The Cart message is the represents a shopping cart for a specific
 * customer.
 */
message Cart {
  repeated LineItem items = 1;
}

/**
 * The CartService shows all rpc methods that the Cart service can handle
 * The names of the rpc methods should match the command handler names in
 * the code.
 */
service CartService {
    /**
     * The SERVICE_TYPE_ENTITY lets Akka Serverless know this service implements
     * an entity
     */
    option (akkaserverless.service) = {
      type: SERVICE_TYPE_ENTITY
      component: ".domain.ShoppingCart"
    };

    /**
     * The AddItem method is called when a new item is added to the cart.
     * The HTTP annotation makes this operation available over HTTP on the
     * mentioned URL. The response is an empty message.
     */
    rpc AddItem(AddLineItem) returns (google.protobuf.Empty) {
        option (google.api.http) = {
            post: "/cart/{user_id}/items/add"
            body: "*"
        };
    }

    /**
     * The RemoveItem method is called when an item is removed from the cart.
     * The HTTP annotation makes this operation available over HTTP on the
     * mentioned URL. The response is an empty message.
     */
    rpc RemoveItem(RemoveLineItem) returns (google.protobuf.Empty) {
        option (google.api.http).post = "/cart/{user_id}/items/{product_id}/remove";
    }

    /**
     * The GetCart method is called when the cart for a user is retrieved.
     * The HTTP annotation makes this operation available over HTTP on the
     * mentioned URL. The response is a Cart message.
     */
    rpc GetCart(GetShoppingCart) returns (Cart) {
        option (google.api.http) = {
            get: "/carts/{user_id}"
            additional_bindings: {
                get: "/carts/{user_id}/items"
                response_body: "items"
            }
        };
    }
}

2. Create the internal interface

  1. Create a domain.proto file at the root of the shoppingcart directory.

  2. Add the syntax version:

    syntax = "proto3";
  3. Add the Akka Serverless import statements:

    import "akkaserverless/annotations.proto";
  4. Add a unique package declaration.

    package ecommerce.persistence;

2.1. Add Event Sourced entity information

Expose the Event Sourced entity to the internal events and state logic in the cart.proto file. The internal messages tell Akka Serverless which message is used to represent the entity state for the internal events.

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

2.2. Add message definitions

Add the internal event message definitions:

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;
}
The internal events relate to a specific entity from the external interface and do not require an entity key.

The completed domain.proto file

The completed domain.proto example:

syntax = "proto3";
import "akkaserverless/annotations.proto";

package ecommerce.persistence;

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

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;
}