1-1-1 Rule
The 1-1-1 rule has the following elements:
- One
proto_library
rule - One source
.proto
file - One top-level entity (message, enum, or extension)
When defining a proto schema, you should have a single message, enum, extension, service, or group of cyclic dependencies per file. This makes refactoring easier. Moving files when they’re separated is much easier than extracting messages from a file with other messages. Following this practice also helps to keep the proto schema files smaller, which enhances maintainability.
One place that modularity of proto schema files is important is when creating gRPC definitions. The following set of proto files shows modular structure.
student_id.proto
edition = "2023";
package my.package;
message StudentID {
string value = 1;
}
full_name.proto
edition = "2023";
package my.package;
message FullName {
string family_name = 1;
string given_name = 2;
}
student.proto
edition = "2023";
package my.package;
import "student_id.proto";
import "full_name.proto";
message Student {
StudentId id = 1;
FullName name = 2;
}
create_student_request.proto
edition = "2023";
package my.package;
import "full_name.proto";
message CreateStudentRequest {
FullName name = 1;
}
create_student_response.proto
edition = "2023";
package my.package;
import "student.proto";
message CreateStudentResponse {
Student student = 1;
}
get_student_request.proto
edition = "2023";
package my.package;
import "student_id.proto";
message GetStudentRequest {
StudentID id = 1;
}
get_student_response.proto
edition = "2023";
package my.package;
import "student.proto";
message GetStudentResponse {
Student student = 1;
}
student_service.proto
edition = "2023";
package my.package;
import "create_student_request.proto";
import "create_student_response.proto";
import "get_student_request.proto";
import "get_student_response.proto";
service StudentService {
rpc CreateStudent(CreateStudentRequest) returns (CreateStudentResponse);
rpc GetStudent(GetStudentRequest) returns (GetStudentResponse);
}
The service definition and each of the message definitions are each in their own file, and you use includes to give access to the messages from other schema files.
In this example, Student
, StudentID
, and FullName
are domain types that
are reusable across requests and responses. The top-level request and response
protos are unique to each service+method.
If you later need to add a middle_name
field to the FullName
message, you
won’t need to update every individual top-level message with that new field.
Likewise, if you need to update Student
with more information, all the
requests and responses get the update. Further, StudentID
might update to be a
multi-part ID.
Lastly, having even simple types like StudentID
wrapped as a message means
that you have created a type that has semantics and consolidated documentation.
For something like FullName
you’ll need to be careful with where this PII gets
logged; this is another advantage of not repeating these fields in multiple
top-level messages. You can tag those fields in one place as sensitive
and exclude them from logging.