My personal reference for learning the basics of service-to-service communications using gRPC.
The repo contains a distributed system that allows a user to request a movie recommendation. The repo has multiple standalone projects, one for each service.
Gradle is used to manage the multi-project build, and Docker-compose enables the full distributed system to built and run locally.
This repo expands on the following tutorial: https://www.cncf.io/blog/2021/08/04/grpc-in-action-example-using-java-microservices/
Pre-requisite of having docker installed locally. See also Docker Desktop.
Start the services:
docker compose up
Using a client like Postman, use server reflection on the following endpoint:
localhost:50051
Make a call
MovieFinderService/getMovie
# message
{
"genre": "ACTION"
}
{
"movie": {
"title": "The Bourne Ultimatum",
"rating": 8,
"genre": "ACTION",
"description": "Spy action-thriller"
}
}
Run servers (in different processes)
# movie-finder service needs to know where other services are running
PORT=50051 \
MOVIE_STORE_SERVER_ENDPOINT=localhost:50052 \
USER_PREFERENCES_SERVER_ENDPOINT=localhost:50053 \
RECOMMENDER_SERVER_ENDPOINT=localhost:50054 \
./gradlew movie-finder:run
PORT=50052 \
./gradlew movie-store:run
PORT=50053 \
./gradlew user-preferences:run
PORT=50054 \
./gradlew recommender:run
- Change/add definitions in
.proto
files e.g. movie-store.proto
syntax = "proto3";
package moviestore;
import "common/common.proto";
option java_package = "com.proto.moviestore";
option java_multiple_files = true;
message MovieStoreRequest {
common.Genre genre = 1;
}
message MovieStoreResponse {
common.Movie movie = 1;
}
service MovieStoreService {
rpc getMovies(MovieStoreRequest) returns (stream MovieStoreResponse) {};
}
- Clean and generate boilerplate classes out of the proto definitions
./gradlew clean
./gradlew generateProto
com.proto.moviestore.MovieStoreRequest;
com.proto.moviestore.MovieStoreResponse;
com.proto.moviestore.MovieStoreServiceGrpc;
- To implement the logic for services, extend the generated
Base
class e.g. MovieStoreServiceImpl
import com.proto.moviestore.MovieStoreRequest;
import com.proto.moviestore.MovieStoreResponse;
import com.proto.moviestore.MovieStoreServiceGrpc;
public class OversimplifiedMovieStoreService extends MovieStoreServiceGrpc.MovieStoreServiceImplBase {
@Override
public void getMovies(MovieStoreRequest request, StreamObserver<MovieStoreResponse> responseObserver) {
// Implement the method
]
}
- Use the service in a server e.g. MovieStoreServer
import com.moviestore.OversimplifiedMovieStoreService;
public class OversimplifiedMovieStoreServer {
public static void main(String[] args) throws IOException, InterruptedException {
Server server = ServerBuilder.forPort(50051)
.addService(new OversimplifiedMovieStoreService())
.build()
.start();
}
}
- To implement logic that calls against a server use the generated client (called a "stub" in gRPC) e.g. implementation of MovieFinderServiceImpl.
import com.proto.moviestore.MovieStoreServiceGrpc;
// "blocking" means it waits for the response before moving onto the next line of code,
//with "non-blocking" the call happens in the background and you need to react to it async (e.g. with an observer)
MovieStoreServiceGrpc.MovieStoreServiceBlockingStub movieStoreClient = MovieStoreServiceGrpc.newBlockingStub(getChannel(movieStoreEndpoint));
movieStoreClient.getMovies(MovieStoreRequest.newBuilder().setGenre(request.getGenre()).build())
.forEachRemaining(response -> {
response.getMovie(); // do something with the response
});
- Build the project
./gradlew build
- Run and call against the server (see Quickstart)
Root project 'movie-grpc'
+--- Project ':movie-api' | common proto definitions used across the full system
+--- Project ':movie-finder' | the main API for the project - demonstrates taking in a request and returning a response
+--- Project ':movie-store' | the "data-layer" of the example (faked with inline data for now) - demonstrates taking in a request and returning a stream
+--- Project ':recommender' | a server "in-the-middle" - demonstrates taking in a stream and returning a response
\--- Project ':user-preferences' | another server "in-the-middle" - demonstrates taking in AND returning a stream
The full order of operations is as follows (not super obvious; pay attention to when streams are opened and closed):
sequenceDiagram
some-client->>+movie-finder: getMovie({ genre })
movie-finder->>+recommender: recommendMovie({ movies })
note over recommender: open recommendMovie stream
movie-finder->>+user-preferences: getShortListedMovies({ userId })
note over user-preferences: open getShortListedMovies stream
movie-finder->>+movie-store: getMovies({ genre })
note over movie-store: open getMovies stream
note over movie-store: get all movies that match the genre
loop for each movie
movie-store->>movie-finder: movie for genre
movie-finder->>user-preferences: move that can be shortlisted
note over user-preferences: determine if move makes the shortlist
opt if shortlisted
user-preferences->>movie-finder: shortlisted movie
movie-finder->>recommender: movie that can be recommended
note over recommender: register movie
end
end
movie-store->>-movie-finder: close getMovies stream
user-preferences->>-movie-finder: close getShortlistedMovies stream
note over recommender: determine recommended movie from registered movies
recommender->>-movie-finder: close stream with recommended movie
movie-finder->>-some-client: recommended Movie```