Skip to content

Commit c5f5529

Browse files
Internal change
PiperOrigin-RevId: 972572890
1 parent 9ec86c9 commit c5f5529

10 files changed

Lines changed: 329 additions & 152 deletions

File tree

content/best-practices/1-1-1.md

Lines changed: 133 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -8,163 +8,230 @@ aliases = "/programming-guides/1-1-1"
88
+++
99

1010
The "1-1-1" best practice advocates structuring definitions with one top-level
11-
entity (message, enum, or extension) per `.proto` file, corresponding to a
12-
single `proto_library` build rule. This approach promotes small, modular proto
13-
definitions. Key benefits include simplified refactoring, potentially improved
14-
build times, and smaller binary sizes due to minimized transitive dependencies.
11+
entry point (exported message, exported enum, service, or extension) per
12+
`.proto` file, corresponding to a single `proto_library` build rule. This
13+
approach promotes small, modular proto definitions. Key benefits include
14+
simplified refactoring, potentially improved build times, and smaller binary
15+
sizes due to minimized transitive dependencies.
1516

16-
## Rationale
17+
## Rationale {#rationale}
1718

18-
The 1-1-1 best practice is to keep every proto_library and .proto file as small
19-
as is reasonable, with the ideal being:
19+
The 1-1-1 best practice is to keep every `proto_library` and `.proto` file as
20+
small as is reasonable, with the ideal ratio being:
2021

2122
* One `proto_library` build rule
2223
* One source `.proto` file
23-
* One top-level entity (message, enum, or extension)
24+
* One top-level entry point (`export message`, `export enum`, `service`, or
25+
`extend`)
2426

25-
Having the fewest number of message, enum, extension, and services as you
26-
reasonably can makes refactoring easier. Moving files when they're separated is
27-
much easier than extracting messages from a file with other messages.
27+
Having the fewest number of exported messages, enums, extensions, and services
28+
as you reasonably can makes refactoring easier. Moving files when they're
29+
separated is much easier than extracting messages from a file with other
30+
messages.
2831

2932
Following this practice can help build times and binary size by reducing the
3033
size of your transitive dependencies in practice: when some code only needs to
31-
use one enum, under a 1-1-1 design it can depend just on the .proto file that
34+
use one enum, under a 1-1-1 design it can depend just on the `.proto` file that
3235
defines that enum and avoid incidentally pulling in a large set of transitive
3336
dependencies that may only be used by another message defined in the same file.
3437

3538
There are cases where the 1-1-1 ideal is not possible (circular dependencies),
3639
not ideal (extremely conceptually coupled messages which have readability
3740
benefits by being co-located), or where some of the downsides don't apply (when
38-
a .proto file has no imports, then there are no technical concerns about the
41+
a `.proto` file has no imports, then there are no technical concerns about the
3942
size of transitive dependencies). As with any best practice, use good judgment
4043
for when to diverge from the guideline.
4144

42-
One place that modularity of proto schema files is important is when creating
43-
gRPC
44-
definitions. The following set of proto files shows modular structure.
45+
## Symbol Visibility in Edition 2024 and Later {#2024-later}
4546

46-
**student_id.proto**
47+
Starting in `edition = "2024"`, Protocol Buffers introduces
48+
[Symbol Visibility](/programming-guides/symbol_visibility)
49+
with `export` and `local` keywords
50+
and the `features.default_symbol_visibility` option.
4751

48-
```proto
49-
edition = "2023";
52+
> **Scope Note:** Symbol visibility controls **only** the Protobuf compiler
53+
> (`protoc`) import behavior when resolving references between `.proto` files.
54+
> It has **no impact** on language-specific generated code or consumers of proto
55+
> descriptors.
5056
51-
package my.package;
57+
### Recommended Setting: `STRICT` {#strict}
5258

53-
message StudentId {
54-
string value = 1;
55-
}
59+
For all new `.proto` files in Edition 2024 and later file-option
60+
`features.default_symbol_visibility` should be set to STRICT. This is the
61+
default in edition 2026:
62+
63+
```proto
64+
option features.default_symbol_visibility = STRICT;
5665
```
5766

58-
**full_name.proto**
67+
In `STRICT` mode:
5968

60-
```proto
61-
edition = "2023";
69+
* All symbols default to `local`.
70+
* Only top-level `message` and `enum` declarations intended as entry points
71+
may be marked with `export`.
72+
* Nested symbols cannot use `export` and are enforced `local` (preventing
73+
accidental imports of nested types across file boundaries).
6274

63-
package my.package;
75+
Top-level messages are designed to be **atomic** with regards to reuse. Nested
76+
messages and enums are inherently private to their parent container. Attempting
77+
to use another message's nested types as reusable vocabulary subverts the "use
78+
all or none" design of `.proto`
79+
files. Types intended for independent reuse should be
80+
defined as top-level symbols in dedicated `.proto` files.
6481

65-
message FullName {
66-
string family_name = 1;
67-
string given_name = 2;
68-
}
69-
```
82+
### Relaxation for `local` Helper Symbols {#relaxation}
7083

71-
**student.proto**
84+
Symbol visibility relaxes strict 1-1-1 for internal helper symbols: a file may
85+
contain multiple `local message` or `local enum` definitions, provided they
86+
support a single top-level entry point (forming a single cluster of `local`
87+
symbols supporting one entry point). Because `local` symbols cannot be imported
88+
into other `.proto` files, they do not introduce transitive dependency bloat to
89+
importers.
90+
91+
### Service Request and Response Messages {#req-res}
92+
93+
Co-locating a `service` and its request/response `message`s in a single file is
94+
acceptable in Edition 2024 if those request and response messages are marked
95+
`local` (or default to `local` under `STRICT` mode). This prevents them from
96+
being imported into other `.proto` files and avoids pulling the `service`
97+
definition and its gRPC dependencies into unrelated build
98+
targets.
99+
100+
However, if a request or response message ever needs to be shared or imported
101+
outside of the service file (for example, for logging, data retention, or reuse
102+
in another RPC), it should be defined as a top-level `export message` in its own
103+
dedicated `.proto` file.
104+
105+
## Modular Schema Example {#example}
106+
107+
One place that modularity of proto schema files is important is when
108+
creating gRPC definitions. The following set of proto
109+
files shows modular structure in Edition 2024.
110+
111+
**student_id.proto**
72112

73113
```proto
74-
edition = "2023";
114+
edition = "2024";
75115
76116
package my.package;
77117
78-
import "student_id.proto";
79-
import "full_name.proto";
118+
option features.default_symbol_visibility = STRICT;
80119
81-
message Student {
82-
StudentId id = 1;
83-
FullName name = 2;
120+
export message StudentId {
121+
string value = 1;
84122
}
85123
```
86124

87-
**create_student_request.proto**
125+
**full_name.proto**
88126

89127
```proto
90-
edition = "2023";
128+
edition = "2024";
91129
92130
package my.package;
93131
94-
import "full_name.proto";
132+
option features.default_symbol_visibility = STRICT;
95133
96-
message CreateStudentRequest {
97-
FullName name = 1;
134+
export message FullName {
135+
string family_name = 1;
136+
string given_name = 2;
98137
}
99138
```
100139

101-
**create_student_response.proto**
140+
**student.proto**
102141

103142
```proto
104-
edition = "2023";
143+
edition = "2024";
105144
106145
package my.package;
107146
108-
import "student.proto";
147+
option features.default_symbol_visibility = STRICT;
109148
110-
message CreateStudentResponse {
111-
Student student = 1;
149+
import "student_id.proto";
150+
import "full_name.proto";
151+
152+
export message Student {
153+
StudentId id = 1;
154+
FullName name = 2;
112155
}
113156
```
114157

115158
**get_student_request.proto**
116159

117160
```proto
118-
edition = "2023";
161+
edition = "2024";
119162
120163
package my.package;
121164
165+
option features.default_symbol_visibility = STRICT;
166+
122167
import "student_id.proto";
168+
import "external/expensive/dependency.proto";
123169
124-
message GetStudentRequest {
170+
// Export and stand-alone because it is used in both
171+
// student_request_log.proto AND student_service.proto.
172+
export message GetStudentRequest {
125173
StudentId id = 1;
174+
expensive.external.OtherMessage message = 2;
126175
}
127176
```
128177

129-
**get_student_response.proto**
178+
**student_request_log.proto**
130179

131180
```proto
132-
edition = "2023";
181+
edition = "2024";
133182
134183
package my.package;
135184
136-
import "student.proto";
185+
option features.default_symbol_visibility = STRICT;
137186
138-
message GetStudentResponse {
139-
Student student = 1;
187+
import "get_student_request.proto";
188+
189+
// log any requests made.
190+
message StudentRequestLog {
191+
string requesting_user = 1;
192+
GetStudentRequest request = 2;
140193
}
141194
```
142195

143196
**student_service.proto**
144197

145198
```proto
146-
edition = "2023";
199+
edition = "2024";
147200
148201
package my.package;
149202
150-
import "create_student_request.proto";
151-
import "create_student_response.proto";
203+
option features.default_symbol_visibility = STRICT;
204+
205+
// imported because we log this message in student_request_log.proto meaning the
206+
// message needs to be in its own file and `export`.
152207
import "get_student_request.proto";
153-
import "get_student_response.proto";
208+
209+
local message GetStudentResponse {
210+
Student student = 1;
211+
}
212+
213+
local message CreateStudentRequest {
214+
FullName name = 1;
215+
}
216+
217+
local message CreateStudentResponse {
218+
Student student = 1;
219+
}
154220
155221
service StudentService {
156222
rpc CreateStudent(CreateStudentRequest) returns (CreateStudentResponse);
157223
rpc GetStudent(GetStudentRequest) returns (GetStudentResponse);
158224
}
159225
```
160226

161-
The service definition and each of the message definitions are each in their own
162-
file, and you use includes to give access to the messages from other schema
163-
files.
227+
The service definition and 3 of its 4 message definitions are defined in
228+
`student_service.proto`. Those messages are all marked `local` making it safe to
229+
co-locate with the `service` definition without introducing unnecessary
230+
dependencies. `GetStudentRequest` is shared by the `StudentService` and
231+
`StudentRequestLog`, and so it should be in its own file.
164232

165-
In this example, `Student`, `StudentId`, and `FullName` are domain types that
166-
are reusable across requests and responses. The top-level request and response
167-
protos are unique to each service+method.
233+
In this example, `Student`, `StudentId`, and `FullName` are public domain types
234+
that are reusable in logs, requests, responses and external protos.
168235

169236
If you later need to add a `middle_name` field to the `FullName` message, you
170237
won't need to update every individual top-level message with that new field.

content/best-practices/dos-donts.md

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -199,13 +199,28 @@ on the [googleapis repository](https://github.com/googleapis/googleapis).
199199

200200
## **Do** Define Message Types in Separate Files {#separate-files}
201201

202-
When defining a proto schema, you should have a single message, enum, extension,
203-
service, or group of cyclic dependencies per file. This makes refactoring
204-
easier. Moving files when they're separated is much easier than extracting
205-
messages from a file with other messages. Following this practice also helps to
206-
keep the proto schema files smaller, which enhances maintainability.
207-
208-
If they will be widely used outside of your project, consider putting them in
202+
When defining a proto schema, you should have a single top-level entry point
203+
(exported message, exported enum, service, or extension) per file. This makes
204+
refactoring easier. Moving files when they're separated is much easier than
205+
extracting messages from a file with other messages. Following this practice
206+
also helps to keep the proto schema files smaller, which enhances
207+
maintainability and reduces transitive dependency bloat.
208+
209+
Symbol visibility (`export` / `local`) relaxes strict separation for internal
210+
helper types: a `.proto` file may contain multiple `local message` or `local
211+
enum` definitions supporting a single top-level entry point (such as RPC
212+
request/response messages supporting a `service`), provided those helper types
213+
remain local and are not imported into other files.
214+
215+
Messages and enums are intended to be **atomic** with regards to reuse. Nested
216+
types (messages or enums defined inside another message) are inherently private
217+
to their parent container. With `STRICT` symbol visibility nested entities
218+
cannot be exported or imported by other `.proto` files at all. You should not
219+
use another message's nested types as reusable vocabulary. Any symbol intended
220+
for independent reuse outside its parent file should be defined as a top-level
221+
`export message` or `export enum` in its own `.proto` file.
222+
223+
If a type will be widely used outside of your project, consider putting them in
209224
their own file with no dependencies. Then it's easy for anyone to use those
210225
types without introducing the transitive dependencies in your other proto files.
211226

0 commit comments

Comments
 (0)