Developing a chat application in Go #1 (Designing GRPC messages)

Building a chat application is a common project to explore concurrent programming and inter-process communication. For an initial implementation, I chose GoLang and gRPC. Go channels are a powerful feature in Go that provide a way for goroutines (concurrent functions) to communicate with each other safely, acting as pipes through which typed values can be sent and received. This makes them particularly suitable for managing the flow of messages in a chat application. gRPC was selected for the Remote Procedure Calls (RPC) because it's a high-performance, open-source framework that works over HTTP/2, enabling efficient communication and supporting streaming, which is essential for real-time chat updates. To handle the message flow and integration with Go channels effectively, I also used the Watermill framework, which is a Go library designed for building message-driven applications and provides abstractions for working with various messaging systems, including Go channels, allowing for more structured and testable message handling.

In this post I will describe the GRPC design of this chat application

Designing GRPC message

message User {
string id = 1;
string username = 2;
string display_name = 3;
string avatar_url = 4;
bool available = 5;
}

The User message defines the schema for a user in our chat system. It encapsulates key profile information and availability status for real-time interactions.

id: A unique identifier automatically generated when a user logs in. This serves as the primary reference key across the system.

username: Provided by the user at sign-up or login. It’s a human-readable, unique name that others can use to find or mention the user.

display_name: A random, friendly display name generated by the system (e.g., "Blue Tiger", "Happy Coder"). This adds personality and abstraction, especially in anonymous or lightweight onboarding flows.

avatar_url: A URL pointing to the user’s profile picture, which can be hosted on your CDN, S3, or an external avatar service like Gravatar.

available: A boolean flag that reflects whether the user is currently available to chat (online presence). This can be toggled based on real-time presence updates (e.g., WebSocket status, activity timer, or manual setting).


message Message {
string id = 1;
string conversation_id = 2;
string sender_id = 3;
string content = 4;
int64 timestamp = 5; // Unix epoch (milliseconds)
string username = 6;
}

The Message message defines the structure of an individual message within a conversation. It includes identifiers, content, metadata, and sender information needed for real-time messaging and UI rendering.

id: A unique identifier for the message. Typically generated using a UUID to ensure global uniqueness.

conversation_id: The ID of the conversation this message belongs to. This field links the message to a thread or group chat.

sender_id: The unique ID of the user who sent the message. This can be used to fetch user details (like avatar or display name) or validate permissions.

content: The actual text content of the message. For basic systems, this is plain text (or encrypted text when we implement the e2e encryption) , but it could later be extended to support attachments, emojis, or rich formatting. When we implement encryption

timestamp: A 64-bit integer representing the message creation time in Unix epoch milliseconds. This allows for precise ordering and is timezone-agnostic.

username: A snapshot of the sender’s username at the time the message was sent. It is needed to reduce the server/client 

message Conversation {
string id = 1;
repeated string participant_ids = 2;
string name = 3; // Optional, for group chats
string last_message_id = 4;
}

The Conversation message defines the structure of a chat thread or channel. It can represent both one-on-one and group chats by storing participant references and metadata like the conversation name and last message sent.

here the most important property is the participant_ids. It is used to identify the participants of this conversation.

service ChatService {
rpc AddUser(AddUserRequest) returns (AddUserResponse);
rpc StreamUsersUpdate(GetAvailableUsersRequest) returns (stream GetAvailableUsersResponse);
rpc SendMessage(SendMessageRequest) returns (SendMessageResponse);
rpc GetConversations(GetConversationsRequest) returns (stream GetConversationsResponse);
rpc GetMessages(GetMessagesRequest) returns (stream GetMessagesResponse);
rpc CreateConversation(CreateConversationRequest) returns (CreateConversationResponse);
}

Then the ChatService describes the RPCs to

AddUser: Registers a new user in the system. Typically called when a user logs in or signs up. One-time call, returns user metadata.

StreamUsersUpdate: Streams real-time updates about users' availability (e.g., online/offline presence). Useful for dynamic contact lists.

SendMessage: Sends a message in a given conversation. Triggers state updates, and possibly pushes via pub/sub.

GetConversations: Streams a list of conversations the user is part of. This is useful starting a new conversation when the one user selects the other user to chat.

GetMessages: Streams messages in a specific conversation, typically sorted by time.

CreateConversation: Starts a new conversation between users. Returns the newly created conversation, or an existing one if it already exists (e.g., for 1:1).


The full implementation can be found here

Comments