Skip to content
Santekno.com | Tech Tutorials and Trends
EN
📖 0%
21 Jun 2025 · 5 min read ·Article 13 / 110
Go

13 Implementing Unary RPC

IH
Ihsan Arif
Writer at Santekno · Backend Engineer

In recent years, the concept of Remote Procedure Call (RPC) has become increasingly popular in backend service development. One of the simplest yet most commonly used patterns is Unary RPC, where the client sends a single request and the server returns a single response. In this article, I’ll walk through the concept of Unary RPC and how to implement it using gRPC in the Go (Golang) environment. Let’s start from the theory, move on to practice, and finish with real-world best practices.


What Is Unary RPC?

Before diving into the code, we need to understand the basic concept. Unary RPC is a one-to-one communication model between client and server:

  • Single request –> Single response

The communication looks very much like a regular function in a programming language, except that the function runs on a different server.

Unary RPC Communication Flow

MERMAID
sequenceDiagram
    participant Client
    participant Server
    Client->>Server: Request (Data)
    Server-->>Client: Response (Data)

Case Study: A Book Management Service

To make things more concrete, we’ll build a simple book management system simulation. The client sends a request to retrieve information about a book by its ID, and the server responds with the details of that book.


Defining the Protobuf

The core of RPC in gRPC is the .proto file, which defines the contract between services.

protobuf
 1// book.proto
 2syntax = "proto3";
 3
 4package bookservice;
 5
 6// Request
 7message GetBookRequest {
 8  string id = 1;
 9}
10
11// Response
12message Book {
13  string id = 1;
14  string title = 2;
15  string author = 3;
16  int32  year = 4;
17}
18
19// Service
20service BookService {
21  rpc GetBook (GetBookRequest) returns (Book);
22}

Once you’ve created this file, generate the stub code with the following command:

bash
1protoc --go_out=. --go-grpc_out=. book.proto

Implementing the Unary RPC Server

Let’s implement the service handler in Golang.

Dummy Database Structure

go
1// book_server.go
2var books = map[string]*bookservice.Book{
3    "1": &bookservice.Book{Id: "1", Title: "Go Programming", Author: "John Doe", Year: 2021},
4    "2": &bookservice.Book{Id: "2", Title: "Distributed Systems", Author: "Jane Roe", Year: 2020},
5}

RPC Handler - GetBook

go
 1type server struct {
 2  bookservice.UnimplementedBookServiceServer
 3}
 4
 5func (s *server) GetBook(
 6    ctx context.Context,
 7    req *bookservice.GetBookRequest,
 8) (*bookservice.Book, error) {
 9    book, ok := books[req.Id]
10    if !ok {
11        return nil, status.Error(codes.NotFound, "book not found")
12    }
13    return book, nil
14}

Main Server

go
 1func main() {
 2    lis, err := net.Listen("tcp", ":50051")
 3    if err != nil {
 4        log.Fatalf("Failed to listen: %v", err)
 5    }
 6
 7    grpcServer := grpc.NewServer()
 8    bookservice.RegisterBookServiceServer(grpcServer, &server{})
 9
10    log.Println("Server running on port :50051")
11    if err := grpcServer.Serve(lis); err != nil {
12        log.Fatalf("Failed to serve: %v", err)
13    }
14}

Implementing the Unary RPC Client

Now let’s simulate a client that sends a request to the server using a specific book ID.

go
 1func main() {
 2    conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
 3    if err != nil {
 4        log.Fatalf("Failed to connect: %v", err)
 5    }
 6    defer conn.Close()
 7
 8    client := bookservice.NewBookServiceClient(conn)
 9    ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
10    defer cancel()
11
12    bookID := "1"
13    res, err := client.GetBook(ctx, &bookservice.GetBookRequest{Id: bookID})
14    if err != nil {
15        log.Fatalf("Error: %v", err)
16    }
17
18    fmt.Printf("Book: %+v\n", res)
19}

Simulation: Request & Response

Let’s take a look at how the communication actually happens.

StepFromToData
Send book requestClientServer{id: "1"}
Look up the bookServer (db)InternalLookup by ID
Send responseServerClient{id: "1", title: "Go Programming", ...}


The Key Communication Flow

With unary RPC, the flow is simple. Here’s the diagram of the request flow:

MERMAID
flowchart TD
    A[Client Kirim Request] --> B{Server Terima}
    B -- Buku Ada --> C[Server Kirim Data Buku]
    B -- Buku Tidak Ada --> D[Server Kirim Error]
    C & D --> E[Client Terima Respons]

This design is highly efficient for operations that truly need just one piece of data per request.


Comparison with Other RPC Types

To make things clearer, here’s a comparison table of the communication models in gRPC:

RPC TypeFromToFlow
UnaryClientServerOne request, one response
Server StreamingClientServerOne request, streamed response
Client StreamingClientServerStreamed request, one response
Bidirectional StreamingClientServerStreamed request, streamed response


Benefits and Best Practices of Unary RPC

Advantages

  • Simple – Similar to using a regular function.
  • Fast – Minimal overhead, no streaming.
  • Well-suited for simple CRUD operations, auth, data lookups, and so on.

Best Practices

  1. Validate Data — Validate the request on the server before processing the data.
  2. Timeout — Use a context timeout on the client to avoid hangs.
  3. Error Handling — Send clear error messages from the server to the client.
  4. Versioning — Consider API versioning from the very start.

Conclusion

Unary RPC is a pattern that is both powerful and simple for microservices. In Go, with gRPC we can implement it in a very efficient and maintainable way. This pattern fits a wide range of use cases, especially remote-call operations that follow a one-request-one-response model.

Have fun experimenting with unary RPC in your own services! If you have any questions or want to discuss further, don’t hesitate to leave a comment below.

Related Articles

💬 Comments