Skip to Content
Course content

199: Defining Protobuf Messages for Ruby Services

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're running a massive international shipping operation. If every warehouse manager just wrote descriptions of the cargo on random pieces of scrap paper in their own handwriting, things would fall apart fast. One person might write "3 boxes of apples," another might write "Apples: 3 units," and a third might just draw a picture of an apple. You'd spend half your day just trying to figure out what's actually in the crates.

To fix this, you implement a Standardized Shipping Manifest. This is a strict, printed form. It has a specific box for "Item ID," a box for "Quantity," and a box for "Weight." Everyone agrees that the first field is always the ID and the second is always the Quantity. It doesn't matter if the manager speaks English or Japanese; the form's structure is the universal language. That's exactly what Protocol Buffers (Protobuf) do for your Ruby services.

Here is how that analogy maps to the code we're about to write:

  • The Manifest Form is your .proto file. It's the "source of truth" that defines exactly what your data looks like.
  • The Boxes on the Form are your message fields. You define the type (integer, string, etc.) and a unique number.
  • The Field Numbers (like = 1) are the secret sauce. Instead of sending the word "quantity" over the wire a million times, Protobuf just sends the number 2, which saves a massive amount of bandwidth.
  • The Actual Cargo is the serialized binary data that travels between your Ruby services.

The Contract: Writing Your First .proto File

In Ruby, we're used to passing around Hashes. They're flexible, sure, but they're "silent killers" in production. If you expect :user_id but get :userId, your code just returns nil and you spend three hours debugging a production crash. Protobuf replaces that guesswork with a strict contract.

Let's say we're building a service that handles Order placements. Instead of a JSON blob, we create a file called order.proto:

syntax = "proto3";

package ecommerce;

message Order {
  int32 order_id = 1;
  string customer_email = 2;
  repeated OrderItem items = 3;
  bool is_priority = 4;
}

message OrderItem {
  int32 product_id = 1;
  int32 quantity = 2;
  float unit_price = 3;
}

Notice the repeated keyword. In Ruby terms, that's just an Array. I used float for the price here for simplicity, though in a real financial system, you'd probably send the price as an integer in cents to avoid floating-point nightmares. I've been burned by that more times than I'd like to admit.

Turning the Blueprint into Ruby Code

The .proto file isn't something Ruby can read directly at runtime. You have to "compile" it. You'll use the protoc compiler with the Ruby plugin to generate a Ruby file. When you run that command, Protobuf generates a class that handles all the heavy lifting of serialization and validation.

Once generated, using it in your service feels very natural. You aren't dealing with raw strings or JSON; you're dealing with actual Ruby objects:

require 'google/protobuf'
require_relative 'order_pb' # This is the file generated by protoc

# Creating a new message
order = Ecommerce::Order.new(
  order_id: 12345,
  customer_email: "dev@example.com",
  is_priority: true
)

# Adding items to our 'repeated' field
order.items << Ecommerce::OrderItem.new(product_id: 101, quantity: 2, unit_price: 19.99)
order.items << Ecommerce::OrderItem.new(product_id: 202, quantity: 1, unit_price: 5.50)

# This is where the magic happens: Serializing to binary
binary_data = Order.encode(order) 
# binary_data is now a compact string of bytes, ready for the wire.

Handling Type Safety and Default Values

One thing that trips people up coming from standard Ruby is how Protobuf handles "missing" data. In a Ruby Hash, a missing key is nil. In Protobuf 3, there is no nil for basic types. If you don't set is_priority, it defaults to false. If you don't set order_id, it defaults to 0.

This is actually a feature, not a bug. It means your Ruby code doesn't have to be littered with if order.order_id.nil? checks. You can trust that the data is always there, even if it's just the default value. If you absolutely need to know if a value was explicitly set or not, you'll have to use "Wrapper types" or the optional keyword (introduced in later versions of proto3), but for 90% of your services, the defaults are your best friend.




📋 Practical Task

Exercise: Building a User Profile Synchronization Message

You are tasked with creating a data contract for a "User Profile Sync" service. Two different Ruby microservices need to exchange user data, and they've agreed to stop using JSON to reduce latency.

Your goal: Create a user_profile.proto file that defines a UserProfile message with the following requirements:

  • A unique user_id (integer).
  • A username (string).
  • An email (string).
  • A list of tags (a repeated string field, e.g., ["premium", "beta-tester", "internal"]).
  • A preferences map where the key is a string (the setting name) and the value is a string (the setting value).
  • An account_status using an enum with three possible states: PENDING, ACTIVE, and SUSPENDED.

After writing the .proto file, write a short Ruby script that:

  1. Instantiates a UserProfile object with sample data.
  2. Adds at least three tags to the profile.
  3. Adds two entries to the preferences map.
  4. Encodes the object into a binary string and prints the size of that string to the console.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.