91. Case Study: A gRPC-Based Todo List Application
91. Case Study: A gRPC-Based Todo List Application
gRPC has become one of the most popular technologies for building high-performance and efficient microservices. In this article, I’ll walk through a simple case study: building a Todo List application using gRPC from scratch. We’ll explore the architecture, the protocol structure, and Go code snippets that you can directly adapt for real-world scenarios.
Why gRPC?
Before diving into the case study, let’s take a moment to discuss why we might choose gRPC:
- Efficiency: gRPC uses Protobuf, which is lighter and faster than traditional JSON/HTTP.
- Interoperability: It supports many languages: Go, Java, C#, Python, and so on.
- Contract-first API: With the
.protofile, the contract between client and server is explicit and strongly-typed. - Streaming: It supports bidirectional data streaming, making it a great fit for real-time use cases.
Todo List Application Architecture
We’ll build the Todo List application with the following architecture:
- Client: A CLI or frontend that sends requests to the gRPC server.
- Server: Handles requests and stores tasks in simple in-memory storage.
Flow Diagram
Let’s illustrate the process of adding and retrieving todos using a mermaid flowchart:
flowchart TD
Client-->|AddTodo|gRPC_Server
gRPC_Server-->|Save data|Storage[In-memory]
Client-->|GetTodos|gRPC_Server
gRPC_Server-->|Fetch todos|Storage
Storage-->|Todos list|gRPC_Server
gRPC_Server-->|Todos response|Client
1. Defining the Protobuf
The .proto file describes the data structures and the service.
1// todo.proto
2syntax = "proto3";
3
4package todo;
5
6// Message Type
7message TodoItem {
8 int32 id = 1;
9 string task = 2;
10 bool completed = 3;
11}
12
13message AddTodoRequest {
14 string task = 1;
15}
16
17message AddTodoResponse {
18 TodoItem item = 1;
19}
20
21message GetTodosRequest {}
22
23message GetTodosResponse {
24 repeated TodoItem items = 1;
25}
26
27// Service
28service TodoService {
29 rpc AddTodo (AddTodoRequest) returns (AddTodoResponse);
30 rpc GetTodos (GetTodosRequest) returns (GetTodosResponse);
31}2. Generate Code from the Proto
Use the Protobuf plugin for Go:
1protoc --go_out=. --go-grpc_out=. todo.proto3. Implementing the gRPC Server in Golang
Create a server.go file:
1package main
2
3import (
4 "context"
5 "log"
6 "net"
7 "sync"
8
9 pb "path/to/generated/todo"
10 "google.golang.org/grpc"
11)
12
13type server struct {
14 pb.UnimplementedTodoServiceServer
15 todos []pb.TodoItem
16 mu sync.Mutex
17 nextID int32
18}
19
20func (s *server) AddTodo(ctx context.Context, req *pb.AddTodoRequest) (*pb.AddTodoResponse, error) {
21 s.mu.Lock()
22 defer s.mu.Unlock()
23 s.nextID++
24 item := pb.TodoItem{
25 Id: s.nextID,
26 Task: req.GetTask(),
27 Completed: false,
28 }
29 s.todos = append(s.todos, item)
30 return &pb.AddTodoResponse{Item: &item}, nil
31}
32
33func (s *server) GetTodos(ctx context.Context, req *pb.GetTodosRequest) (*pb.GetTodosResponse, error) {
34 s.mu.Lock()
35 defer s.mu.Unlock()
36 // Copy the slice for concurrent safety
37 items := make([]*pb.TodoItem, len(s.todos))
38 for i, todo := range s.todos {
39 copied := todo
40 items[i] = &copied
41 }
42 return &pb.GetTodosResponse{Items: items}, nil
43}
44
45func main() {
46 lis, err := net.Listen("tcp", ":50051")
47 if err != nil {
48 log.Fatalf("failed to listen: %v", err)
49 }
50 s := grpc.NewServer()
51 pb.RegisterTodoServiceServer(s, &server{})
52 log.Println("gRPC server running at :50051")
53 if err := s.Serve(lis); err != nil {
54 log.Fatalf("failed to serve: %v", err)
55 }
56}4. Implementing a Simple Client
Create a client.go file:
1package main
2
3import (
4 "context"
5 "log"
6 "time"
7
8 pb "path/to/generated/todo"
9 "google.golang.org/grpc"
10)
11
12func main() {
13 conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
14 if err != nil {
15 log.Fatalf("did not connect: %v", err)
16 }
17 defer conn.Close()
18 c := pb.NewTodoServiceClient(conn)
19
20 // Simulate AddTodo
21 ctx, cancel := context.WithTimeout(context.Background(), time.Second)
22 defer cancel()
23 r, err := c.AddTodo(ctx, &pb.AddTodoRequest{Task: "Buy coffee"})
24 if err != nil {
25 log.Fatalf("could not add: %v", err)
26 }
27 log.Printf("Added Todo: %v", r.GetItem())
28
29 // Simulate GetTodos
30 resp, err := c.GetTodos(ctx, &pb.GetTodosRequest{})
31 if err != nil {
32 log.Fatalf("could not get todos: %v", err)
33 }
34 for _, t := range resp.GetItems() {
35 log.Printf("%d: %s [completed: %v]", t.GetId(), t.GetTask(), t.GetCompleted())
36 }
37}5. Simulating Client-Server Interaction
Let’s imagine a simulation of adding two todos and then fetching the todo list:
1. Add “Buy coffee”
Client → Server: AddTodo("Buy coffee")
Server adds: {id: 1, task: 'Buy coffee', completed: false}
2. Add “Morning walk”
Client → Server: AddTodo("Morning walk")
Server adds: {id: 2, task: 'Morning walk', completed: false}
3. GetTodos
Client → Server: GetTodos()
Server response:
| id | task | completed |
|---|---|---|
| 1 | Buy coffee | false |
| 2 | Morning walk | false |
6. Strengths and Limitations
Let’s compare a gRPC-based Todo application against a simple REST one in a table:
| Feature | REST API | gRPC |
|---|---|---|
| Serialization | JSON | Protobuf (binary) |
| Speed | Medium | Very Fast |
| Data Typing | Weakly-typed (runtime) | Strongly-typed (compile) |
| Streaming | Not a good fit | Native bidi support |
| Contract Documentation | Swagger/OpenAPI | .proto (more consistent) |
| Interoperability | High | High |
| Browser Support | Native (can fetch) | No, requires a gateway |
Drawbacks of gRPC
- A browser client must go through a proxy/gateway.
- The initial communication setup is a bit more complex.
- For small/monolithic applications, REST may be “enough.”
7. Conclusion
gRPC is the right choice for systems that need performance, strong data typing, or inter-service communication under heavy load. This Todo List case study demonstrates just how economical, expressive, and scalable gRPC can be.
If you want to explore further, try adding streaming to the status-update feature (CompleteTodo over a stream), or integrate a real database such as PostgreSQL.
Further Learning Resources
Let’s get started refactoring your REST API to gRPC for a more optimal and scalable experience! 🚀