Consider the example of creating a BFF ( FederationService ) that combines and returns the results obtained using the PostService and UserService.
This example’s architecture is following.
- End-User sends a gRPC request to
FederationServicewithpost id FederationServicesends a gRPC request toPostServicemicroservice withpost idto getPostmessageFederationServicesends a gRPC request toUserServicemicroservice with theuser_idpresent in thePostmessage and retrieves theUsermessageFederationServiceaggregatesPostandUsermessages and returns it to the End-User as a single message
The Protocol Buffers file definitions of PostService and UserService and FederationService are as follows.
PostService has GetPost gRPC method. It returns Post message.
- post.proto
package post;
service PostService {
rpc GetPost (GetPostRequest) returns (GetPostReply) {}
}
message GetPostRequest {
string post_id = 1;
}
message GetPostReply {
Post post = 1;
}
message Post {
string id = 1;
string title = 2;
string content = 3;
string user_id = 4;
}UserService has GetUser gRPC method. It returns User message.
- user.proto
package user;
service UserService {
rpc GetUser (GetUserRequest) returns (GetUserReply) {}
}
message GetUserRequest {
string user_id = 1;
}
message GetUserReply {
User user = 1;
}
message User {
string id = 1;
string name = 2;
int64 age = 3;
}FederationService has a GetPost method that aggregates the Post and User messages retrieved from PostService and UserService and returns it as a single message.
- federation.proto
package federation;
service FederationService {
rpc GetPost (GetPostRequest) returns (GetPostReply) {}
}
message GetPostRequest {
string id = 1;
}
message GetPostReply {
Post post = 1;
}
message Post {
string id = 1;
string title = 2;
string content = 3;
User user = 4;
}
message User {
string id = 1;
string name = 2;
int64 age = 3;
}grpc.federation.service option is used to specify a service to generated target using gRPC Federation.
Therefore, your first step is to import the gRPC Federation proto file and add the service option.
+import "grpc/federation/federation.proto";
service FederationService {
+ option (grpc.federation.service) = {};
rpc GetPost (GetPostRequest) returns (GetPostReply) {}
}gRPC Federation focuses on the response message of the gRPC method.
Therefore, add an option to the GetPostReply message, which is the response message of the GetPost method, to describe how to construct the response message.
- federation.proto
message GetPostReply {
+ option (grpc.federation.message) = {};
Post post = 1;
}In the gRPC Federation, grpc.federation.message option creates a variable and grpc.federation.field option refers to that variable and assigns a value to the field.
So first, we use def to define variables.
- federation.proto
message GetPostReply {
option (grpc.federation.message) = {
+ def {
+ name: "p"
+ message {
+ name: "Post"
+ args { name: "pid", by: "$.id" }
+ }
+ }
};
Post post = 1;
}The above definition is equivalent to the following pseudo Go code.
// getGetPostReply returns GetPostReply message by GetPostRequest message.
func getGetPostReply(req *pb.GetPostRequest) *pb.GetPostReply {
p := getPost(&PostArgument{Pid: req.GetId()})
...
}
// getPost returns Post message by PostArgument.
func getPost(arg *PostArgument) *pb.Post {
postID := arg.Pid
...
}name: "p": It means to create a variable namedpmessage {}: It means to get message instancename: "Post": It means to getPostmessage infederationpackage.args: {name: "pid", by: "$.id"}: It means to retrieve the Post message, to pass as an argument a value whose name ispidand whose value is$.id.
$.id indicates a reference to a message argument. The message argument for a GetPostReply message is a GetPostRequest message. Therefore, the "$." can be used to refer to each field of GetPostRequest message.
For more information on each feature, please refer to the API Reference
Assigns the value using grpc.federation.field option to a field ( field binding ).
p variable type is a Post message type and Post post = 1 field also Post message type. Therefore, it can be assigned as is without type conversion.
message GetPostReply {
option (grpc.federation.message) = {
def {
name: "p"
message {
name: "Post"
args { name: "pid", by: "$.id" }
}
}
};
+ Post post = 1 [(grpc.federation.field).by = "p"];
}To create GetPostReply message, Post message is required. Therefore, it is necessary to define how to create Post message, by adding an gRPC Federation's option to Post message as in GetPostReply message.
message GetPostReply {
option (grpc.federation.message) = {
def {
name: "p"
message {
name: "Post"
args { name: "pid", by: "$.id" }
}
}
};
Post post = 1 [(grpc.federation.field).by = "p"];
}
message Post {
option (grpc.federation.message) = {
// call post.PostService/GetPost method with post_id and binds the response message to `res` variable
def {
name: "res"
call {
method: "post.PostService/GetPost"
request { field: "post_id", by: "$.pid" }
}
}
// refer to `res` variable and access post field.
// Use autobind feature with the retrieved value.
def {
by: "res.post"
autobind: true
}
};
string id = 1; // binds the value of `res.post.id` by autobind feature
string title = 2; // binds the value of `res.post.title` by autobind feature
string content = 3; // binds the value of `res.post.content` by autobind feature
User user = 4; // TODO
}In def, besides getting the message, you can call the gRPC method and assign the result to a variable, or get another value from the value of a variable and assign it to a new variable.
The first def in the Post message calls post.PostService's GetPost method and assigns the result to the res variable.
The second def in the Post message access post field of res variable and use autobind feature for easy field binding.
Tip
If the defined value is a message type and the field of that message type exists in the message with the same name and type, the field binding is automatically performed. If multiple autobinds are used at the same message, you must explicitly use the grpc.federation.field option to do the binding yourself, since duplicate field names cannot be correctly determined as one.
Finally, since Post message depends on User message, add an option to User message.
message Post {
option (grpc.federation.message) = {
def {
name: "res"
call {
method: "post.PostService/GetPost"
request { field: "post_id", by: "$.pid" }
}
}
def {
// the value of `res.post` assigns to `post` variable.
name: "post"
by: "res.post"
autobind: true
}
// get User message and assign it to `u` variable.
// The `post` variable is referenced to retrieve the `user_id`, and the value is named `uid` as an argument for User message.
def {
name: "u"
message {
name: "User"
args { name: "uid", by: "post.user_id" }
}
}
};
string id = 1;
string title = 2;
string content = 3;
// binds `u` variable to user field.
User user = 4 [(grpc.federation.field).by = "u"];
}
message User {
option (grpc.federation.message) = {
def [
{
name: "res"
call {
method: "user.UserService/GetUser"
// refer to message arguments with `$.uid`
request { field: "user_id", by: "$.uid" }
}
},
{
by: "res.user"
autobind: true
}
]
};
string id = 1;
string name = 3;
int age = 3;
}The final completed proto definition will look like this.
- federation.proto
package federation;
import "grpc/federation/federation.proto";
import "post.proto";
import "user.proto";
service FederationService {
option (grpc.federation.service) = {};
rpc GetPost (GetPostRequest) returns (GetPostReply) {}
}
message GetPostRequest {
string id = 1;
}
message GetPostReply {
option (grpc.federation.message) = {
def {
name: "p"
message {
name: "Post"
args { name: "pid", by: "$.id" }
}
}
};
Post post = 1 [(grpc.federation.field).by = "p"];
}
message Post {
option (grpc.federation.message) = {
def {
name: "res"
call {
method: "post.PostService/GetPost"
request { field: "post_id", by: "$.pid" }
}
}
def {
name: "post"
by: "res.post"
autobind: true
}
def {
name: "u"
message {
name: "User"
args { name: "uid", by: "post.user_id" }
}
}
};
string id = 1;
string title = 2;
string content = 3;
User user = 4 [(grpc.federation.field).by = "u"];
}
message User {
option (grpc.federation.message) = {
def [
{
name: "res"
call {
method: "user.UserService/GetUser"
request { field: "user_id", by: "$.uid" }
}
},
{
by: "res.user"
autobind: true
}
]
};
string id = 1;
string name = 3;
int age = 3;
}Next, generates gRPC server codes using by this federation.proto.
First, install grpc-federation-generator
go install github.com/mercari/grpc-federation/cmd/grpc-federation-generator@latestPuts federation.proto, post.proto, user.proto files to under the proto directory.
Also, write grpc-federation.yaml file to run generator.
- grpc-federation.yaml
imports:
- proto
src:
- proto
out: .Run code generator by the following command.
grpc-federation-generator ./proto/federation.protoRunning code generation using the federation.proto will create a federation_grpc_federation.pb.go file under the output path.
In federation_grpc_federation.pb.go, an initialization function ( NewFederationService ) for the FederationService is created. The server instance initialized using that function can be registered as a gRPC server using the RegisterFederationService function defined in federation_grpc.pb.go as is.
When initializing, you need to create a dedicated Config structure and pass.
type ClientConfig struct{}
func (c *ClientConfig) Post_PostServiceClient(cfg federation.FederationServiceClientConfig) (post.PostServiceClient, error) {
// create by post.NewPostServiceClient()
...
}
func (c *ClientConfig) User_UserServiceClient(cfg federation.FederationServiceClientConfig) (user.UserServiceClient, error) {
// create by user.NewUserServiceClient()
...
}
federationServer, err := federation.NewFederationService(federation.FederationServiceConfig{
// Client provides a factory that creates the gRPC Client needed to invoke methods of the gRPC Service on which the Federation Service depends.
// If this interface is not provided, an error is returned during initialization.
Client: new(ClientConfig),
})
if err != nil { ... }
grpcServer := grpc.NewServer()
federation.RegisterFederationServiceServer(grpcServer, federationServer)Config (e.g. federation.FederationServiceConfig ) must always be passed a configuration to initialize the gRPC Client, which is needed to invoke the methods on which the federation service depends.
Also, there are settings for customizing error handling on method calls or logger, etc.
