Writing gRPC descriptors

Akka Serverless SDKs support protobuf descriptors in the Proto3 new tab Protocol Buffers language. You define command messages, data associated with Entities, and events in .proto files. From these definitions, the gRPC compiler creates client and server side code that saves work for you and enables Akka Serverless to serialize message data.

Note that Value Entities and Event Sourced Entities are slightly different based upon codegen requirements. Other than the code generation annotations, the proto files are materially equivalent for the same use-case. See the following examples which show the difference.

Event Sourced Entity
// Describes how this domain relates to an event sourced entity
option (akkaserverless.file).event_sourced_entity = {
  name: "ShoppingCart"
  entity_type: "eventsourced-shopping-cart"
  state: "Cart"
  events: "ItemAdded"
  events: "ItemRemoved"
};
Value Entity
// Describes how this domain relates to a value entity
option (akkaserverless.file).value_entity = {
  name: "ShoppingCart"
  entity_type: "shopping-cart"
  state: "Cart"
};

We recommend that you define your service API and events and data associated with components separately. This allows business logic to evolve independently of the public interface. This page walks you through elements in an example shoppingcart_api.proto file and the associated shoppingcart_domain.proto file.

The service API proto file

The first line of an example shoppingcart_api.proto file defines the version of Protocol Buffer syntax:

proto/shoppingcart_api.proto
syntax = "proto3";

The following imports provide Akka Serverless and gRPC functionality:

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

The package specifies a namespace for this proto file and its imports—​names must be unique within each namespace:

proto/shoppingcart_api.proto
package com.example.shoppingcart;

Messages

You define messages that can be sent to or returned from the service. Each message for an Entity that is input to a RPC command, must contain an Entity key. In the example, this includes AddLineItem, RemoveLineItem, and GetShoppingCart where the user_id is the (akkaserverless.field).entity_key.

Akka Serverless uses the entity_key to know which instance of an Entity a message is for. If more than one field is specified as an Entity key, the fields are concatenated together. Akka Serverless serializes Entity keys to strings.

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

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

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

message RemoveShoppingCart {
  string cart_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;
}

To use a multi-field key, add the entity_key notation to each field. For example, this SomeMessage element defines both first_field and second_field as part of the key:

message SomeMessage {
  string first_field = 1 [(akkaserverless.field).entity_key = true];
  string second_field = 2 [(akkaserverless.field).entity_key = true];

Service

This section of the .proto file declares the API of the service itself, along with each function or method and their parameters and return types. When a command is received for a given Entity key, Akka Serverless will establish a gRPC streamed call to the service implementation using that Entity’s type’s protocol—​if one isn’t already established. Any commands received for the Entity key will be sent through that call.

The AddItem and RemoveItem methods have no return value (the Empty type).

See Transcoding HTTP for an explanation of the HTTP annotations.

proto/shoppingcart_api.proto
service ShoppingCartService {
  option (akkaserverless.service) = {
    type: SERVICE_TYPE_ENTITY
    component: ".domain.ShoppingCart"
  };

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

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

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

  rpc RemoveCart (RemoveShoppingCart) returns (google.protobuf.Empty) {
    option (google.api.http).post = "/carts/{cart_id}/remove";
  }
}
1 See Transcoding HTTP below
2 See Transcoding HTTP below
3 See Transcoding HTTP below

The domain proto file

The shoppingcart_domain.proto file specifies the state and messages for an Event Sourced Entity. It defines the gRPC version:

proto/shoppingcart_domain.proto
syntax = "proto3";

It only needs to include the import for akkaserverless/annotations.proto:

proto/shoppingcart_domain.proto
import "akkaserverless/annotations.proto";

The following defines the ShoppingCart Entity state:

proto/shoppingcart_domain.proto
// Describes how this domain relates to a value entity
option (akkaserverless.file).value_entity = {
  name: "ShoppingCart"
  entity_type: "shopping-cart"
  state: "Cart"
};

It includes the messages that define the entity state:

proto/shoppingcart_domain.proto
message LineItem {
  string productId = 1;
  string name = 2;
  int32 quantity = 3;
}

// The shopping cart state.
message Cart {
  repeated LineItem items = 1;
}

Transcoding HTTP

Akka Serverless supports transcoding gRPC to HTTP/JSON, using the Google transcoding annotations described here. You can use transcoding to consume your Entities' gRPC interfaces using HTTP/JSON.

In the example below, the optional transcoding of the service to bind the various endpoints to HTTP is highlighted with annotations.

service ShoppingCartService {
  option (akkaserverless.service) = {
    type: SERVICE_TYPE_ENTITY
    component: ".domain.ShoppingCart"
  };

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

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

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

  rpc RemoveCart (RemoveShoppingCart) returns (google.protobuf.Empty) {
    option (google.api.http).post = "/carts/{cart_id}/remove";
  }
}
1 This extra annotation specifies that you can call this endpoint using the POST method with the URI /cart/{user_id}/items/add, where {user_id} is the actual user id we want the cart for.
2 A URL that accepts a POST method to remove a line item.
3 A more complex example where the first get URI retrieves the whole cart, and the second retrieves the items in the cart.