44 Reusing Messages with `import`
When building distributed systems or microservices, exchanging data between services becomes a necessity. Protocols like gRPC, which are based on Protocol Buffers (protobuf), are extremely popular thanks to their fast serialization and explicit schema representation. However, the larger the data schema you manage, the more critical it becomes to organize and reuse that schema effectively. One of protobuf’s standout features that often gets underrated is the ability to import and reuse messages across protobuf files.
This article explores the concept of reusing messages with import in Protobuf, how to use it, and the best practices for organizing your data schema. I’ll also include code examples, a simulation of how it works in a real-world project, a comparison table of implementations, and even a Mermaid flow diagram to clarify how reusability with import works.
1. A Common Problem: Schema Duplication That Doesn’t Scale
Imagine you have two different teams: the Payments team and the User Management team. Both need a definition of the User message, for example:
1// payments.proto
2message User {
3 string id = 1;
4 string email = 2;
5}1// users.proto
2message User {
3 string id = 1;
4 string email = 2;
5}The approach above looks simple, but as the number of services and message definitions grows, this duplication creates problems:
- Inconsistent schema: A field gets changed in one file, but the other services don’t pick up the update.
- Code maintenance nightmare: Every update to a duplicated definition has to be repeated across many files.
- Riskier deployments: The chance of errors increases whenever schemas fall out of sync.
So what’s the solution? This is exactly where protobuf’s import feature comes into play.
2. The Core Concept: Reuse with import
Protocol Buffers supports imports much like modern programming languages do. You simply define a message once and then reuse it across various other .proto files.
Directory structure:
1proto/
2 user.proto
3 payment.proto
4 order.protouser.proto
1syntax = "proto3";
2
3package myapp.user;
4
5message User {
6 string id = 1;
7 string email = 2;
8}payment.proto
1syntax = "proto3";
2
3package myapp.payment;
4
5import "user.proto";
6
7message Payment {
8 string payment_id = 1;
9 myapp.user.User user = 2; // reuse the message from user.proto
10 double amount = 3;
11}With this setup, the User message is defined once and can be used anywhere simply by importing it.
3. Project Scenario Simulation: A Multi-Service Setup
Suppose your company has the following services:
| Service Name | Proto File | Reused Message |
|---|---|---|
| User Service | user.proto | User |
| Payment Service | payment.proto | User (from user.proto) |
| Order Service | order.proto | User (from user.proto) |
❗ Note: All services must agree on a consistent package and protocol path so that the protobuf compiler (protoc) can resolve every dependency.
Compile Process Simulation:
Each developer simply runs:
1protoc --proto_path=proto/ --go_out=. payment.protoprotoc will automatically locate and compile user.proto if it’s found in the same directory path or has been included via the --proto_path option.
4. Flow Diagram: How Does import Work?
Let’s visualize how message reuse flows when protoc runs.
graph LR
A[service.proto] -->|"import "user.proto""| B[user.proto]
B --> C[message User didefinisikan]
A --> D[message Payment didefinisikan]
D -->|menggunakan User| C
A & B --> E[protoc compiler]
E --> F[Generated code dalam masing-masing bahasa]
Explanation:
- The
payment.protofile importsuser.proto. - The
Usermessage is defined only once inuser.proto. - The
Paymentmessage usesUser(fully qualified asmyapp.user.Userwhen it’s in a different package). protocresolves the dependencies across all files.
5. Case Study: Changing the User Schema Centrally
Let’s say a full name field needs to be added to User:
1// user.proto
2message User {
3 string id = 1;
4 string email = 2;
5 string full_name = 3;
6}Without touching any other file, every service that imports user.proto now automatically supports this new field after recompiling!
6. Table: Comparison Without vs. With import
| Aspect | Without import | With import |
|---|---|---|
| Schema Consistency | Maintain each file manually | Everything inherits from one source |
| Schema Updates | Risk of falling out of sync | Just change a single proto file |
| Maintenance | Harder | Easy and scalable |
| IDE Support | Difficult to lint cross-file | Better auto-completion |
| Dependency Build | Manual/linking | Dependencies resolved automatically |
7. Best Practices for Organizing Protobuf
Here are some best practices to get the most out of import:
- Separate shared type schemas into their own files.
- For example: keep each
User,Product, andAddressmessage in its own proto file.
- For example: keep each
- Use clear, normalized namespace/package naming.
- Document the schemas shared between services in a central place.
- Version your proto files (e.g., user_v1.proto / user_v2.proto) when introducing breaking changes.
- Keep dependencies between protos free of cycles (avoid circular imports).
8. Advanced: Reusing Enums, Services, and Extensions with import
import isn’t limited to messages. Enums, services, and even extension fields can be reused as well:
1// status.proto
2enum Status {
3 ACTIVE = 0;
4 INACTIVE = 1;
5}
6
7// user.proto
8import "status.proto";
9message User {
10 ...
11 Status status = 4;
12}9. Conclusion
The import feature in Protobuf is a key tool for reusing schemas in a way that’s scalable, maintainable, and reduces code duplication. By designing a modular proto structure, placing reusable messages in a single location, and staying disciplined about updates and documentation, even large projects remain robust and easy to manage.
If your project is starting to “smell” because of message duplication, now is the time to refactor and harness the power of import in Protobuf!
Have you started reusing messages with import yet? Share your experience in the comments! 🚀