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
FederationService
withpost id
FederationService
sends a gRPC request toPostService
microservice withpost id
to getPost
messageFederationService
sends a gRPC request toUserService
microservice with theuser_id
present in thePost
message and retrieves theUser
messageFederationService
aggregatesPost
andUser
messages 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;
int 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 = 3;
int 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 namedp
message {}
: It means to get message instancename: "Post"
: It means to getPost
message infederation
package.args: {name: "pid", by: "$.id"}
: It means to retrieve the Post message, to pass as an argument a value whose name ispid
and 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@latest
Puts 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.proto
Running 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.