Create proto files

Messages in Akka Serverless get persisted in protobuf format. You’ll need to define messages and events in a .proto file along with a service that specifies the remote procedure calls. As a best practice, the external interface, or API, should be kept separate from the internal state model so the state model can evolve without impacting clients. To accomplish this, you will create separate .proto. files for them.

The external interface

The external interface is used to expose your service to the outside world. Those could be other services in your project or services outside of Akka Serverless.

I. Create the file for the external interface

Start your .proto file:

  1. Create a cart.proto file in the shoppingcart directory.

  2. To specify the syntax version, add the following on the first line of the file:

    syntax = "proto3";
  3. The example uses Google and Akka Serverless functionality. Add the following import statements:

    import "akkaserverless/annotations.proto";
    import "google/api/annotations.proto";
    import "google/protobuf/empty.proto";
    • The google imports allow you to send empty responses and add annotations to expose the service with a REST interface.

    • The akkaserverless imports add event sourcing and key capabilities.

  4. To prevent naming conflicts, add a package declaration:

    package ecommerce;

II. Add message definitions

The messages you define in cart.proto are received by your service as requests and are sent back as responses. Message definitions are aggregates that contain a set of typed fields. Each entity requires a key, which is defined by the annotation [(.akkaserverless.field.entity_key) = true]. The entity identifies which instance of an entity a command is for.

  1. Add message definitions corresponding to the functionality needed by a shopping cart:

    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 above example, the message AddLineItem contains fields for, among others, user_id and product_id. The ` = 1`, ` = 2` at the end of the lines are the unique "tags" that field uses in the binary encoding.

III. Add the service

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

  1. Add the CartService service as follows:

    service CartService {
      option (akkaserverless.service).entity.type = ".persistence.Cart";
    }
  2. In the CartService service, 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 CartService service, 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 CartService service, 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

Your complete cart.proto file should look like the following:

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

package ecommerce;

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;
}
service CartService {
    option (akkaserverless.service).entity.type = ".persistence.Cart";

    rpc AddItem(AddLineItem) returns (google.protobuf.Empty) {
        option (google.api.http) = {
            post: "/cart/{user_id}/items/add"
            body: "*"
        };
    }

    rpc RemoveItem(RemoveLineItem) returns (google.protobuf.Empty) {
        option (google.api.http).post = "/cart/{user_id}/items/{product_id}/remove";
    }

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

The internal events

The internal events are, as a best practice, modeled separately. This means your API (external interface) and your internal events can evolve independently and changes to your API will not require you to make changes to the way you handle data.

I. Create the file for the external interface

Start your .proto file:

  1. Create a domain.proto file in the shoppingcart directory.

  2. To specify the syntax version, add the following on the first line of the file:

    syntax = "proto3";
  3. In this file you only need the Akka Serverless functionality. Add the following import statements:

    import "akkaserverless/annotations.proto";
  4. To prevent naming conflicts, add a package declaration. This package is named differently to avoid conflicts with your API:

    package ecommerce.persistence;

II. Add Event Sourced entity information

The Event Sourced entity needs to know how the internal events and state relate to the entity itself. To make that connection, you’ll need to add the code below. This tells Akka Serverless which message is used to represent the state of your entity and which messages are used to represent the internal events.

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

III. Add message definitions

The messages you define in domain.proto are your internal events. Those events are already related to a specific entity, so there is no entity key needed in these messages.

  1. Add message definitions corresponding to the events needed by a shopping cart:

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