Skip to content

Commit c90c91f

Browse files
beeme1mrtoddbaert
andauthored
docs: refines the code default adr (#1648)
## This PR - updates the code default ADR to use `FLAG_NOT_FOUND` errors codes instead of an undefined value. ### Notes This update will have nearly identical end results but will be easier to implement and more consistent. Core to this change is switching from using an empty value as the indicator to using the `FLAG_NOT_FOUND` error code. While this may seem odd at first, it's in line with the behavior a user sees when first releasing a disabled flag. This will allow users to `create a new disabled flag` -> `enable the flag for a subset of users without impacting others` -> `enable the flag for everyone` -> `deprecate the flag`. --------- Signed-off-by: Michael Beemer <beeme1mr@users.noreply.github.com> Co-authored-by: Todd Baert <todd.baert@dynatrace.com>
1 parent 0642ac8 commit c90c91f

File tree

1 file changed

+27
-158
lines changed

1 file changed

+27
-158
lines changed

docs/architecture-decisions/support-code-default.md

Lines changed: 27 additions & 158 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
---
2-
# Valid statuses: draft | proposed | rejected | accepted | superseded
32
status: accepted
43
author: @beeme1mr
54
created: 2025-06-06
@@ -77,101 +76,23 @@ The absence of a value field provides an unambiguous signal that distinguishes b
7776
}
7877
```
7978

80-
2. **Protobuf Considerations**:
81-
82-
**No proto changes required** - The existing proto3 definitions already support this behavior:
83-
84-
```protobuf
85-
message ResolveBooleanResponse {
86-
bool value = 1;
87-
string reason = 2;
88-
string variant = 3;
89-
google.protobuf.Struct metadata = 4;
90-
}
91-
```
92-
93-
In proto3, all fields are optional by default. To implement code default delegation:
94-
- Do NOT set the `value` field (not even to false/0/"")
95-
- Do NOT set the `variant` field
96-
- Set `reason` to "DEFAULT"
97-
- Optionally set `metadata`
98-
99-
**Critical implementation notes**:
100-
- Ensure flagd omits fields rather than setting zero values
101-
- Different languages detect field presence differently:
102-
- Go: Check for zero values or use pointers
103-
- Java: Use `hasValue()` / `hasVariant()` methods
104-
- Python: Use `HasField()` or check field presence
105-
- JavaScript: Check for `undefined` (not null)
106-
- Consider adding proto comments documenting this behavior
107-
- Test wire format to confirm fields are actually omitted
108-
109-
**Example - Correct vs Incorrect Implementation**:
110-
111-
```go
112-
// INCORRECT - sets zero value
113-
response := &ResolveBooleanResponse{
114-
Value: false, // This sends 'false' on the wire
115-
Reason: "DEFAULT",
116-
Variant: "", // This sends empty string on the wire
117-
}
118-
119-
// CORRECT - omits fields
120-
response := &ResolveBooleanResponse{
121-
Reason: "DEFAULT",
122-
// Value and Variant fields are not set at all
123-
}
124-
```
125-
126-
3. **Evaluation Behavior**:
79+
2. **Evaluation Behavior**:
12780
- When flag has `defaultVariant: null` and targeting returns no match
128-
- Server responds with both value and variant fields omitted, reason set to "DEFAULT"
129-
- Client detects the missing value field and uses its code-defined default
81+
- Server responds with reason set to reason "ERROR" and error code "FLAG_NOT_FOUND"
82+
- Client detects this reason value field and uses its code-defined default
13083
- This same pattern works across all evaluation modes
13184

132-
**In-Process Mode Special Considerations**:
133-
- The in-process evaluator must return the same "shape" of response
134-
- Cannot return null/nil as that's different from "no value"
135-
- May need a special response type or wrapper to indicate delegation
136-
- Example approach: Return evaluation result with a "useDefault" flag
137-
138-
4. **Remote Evaluation Protocol Responses**:
139-
140-
**OFREP (OpenFeature Remote Evaluation Protocol)**:
141-
- Return HTTP 200 with response body that omits both value and variant fields
142-
- Reason field set to "DEFAULT" to indicate delegation
143-
- Clear distinction from error cases (which return 4xx/5xx)
144-
145-
**flagd RPC**:
146-
- Omit both the type-specific value field and variant field in response messages
147-
- Use protobuf field presence to signal "no opinion from server"
148-
- No changes needed to RPC method signatures
149-
150-
5. **Provider Implementation**:
151-
- Check for presence/absence of value field in responses
152-
- When value is absent and reason is "DEFAULT", use code-defined default
153-
- When value is present (even if null/false/empty), use that value
154-
- Variant field will also be absent in delegation responses, resulting in undefined variant in resolution details
155-
- Responses with value fields work as before, maintaining backward compatibility
85+
3. **Provider Implementation**:
86+
- No changes to existing providers
15687

15788
### Design Rationale
15889

159-
**Using "DEFAULT" reason**: We intentionally reuse the existing "DEFAULT" reason code rather than introducing a new one (like "CODE_DEFAULT"). The distinction between a configuration default and code default is clear from the response structure:
160-
161-
- Configuration default: Has both value and variant fields
162-
- Code default: Omits both value and variant fields
163-
164-
**Field Omission Pattern**: Using field presence/absence is a well-established pattern in protocol design:
90+
**Using "ERROR" reason**: We intentionally reuse the existing "ERROR" reason code rather than introducing a new one (like "CODE_DEFAULT"). This retains the current behavior of an disabled flag and allows for progressive enablement of a flag without unexpected variations in flag evaluation behavior.
16591

166-
- Unambiguous: Cannot confuse "null value" with "no opinion"
167-
- Language agnostic: Works across type systems
168-
- Protocol friendly: Natural in both JSON and Protobuf
169-
- Backward compatible: Existing responses always include values
170-
- Spec compliant: OpenFeature allows undefined variants
92+
Advantages of this approach:
17193

172-
The omission of both value and variant fields creates a clear, consistent signal that the server is fully delegating the decision to the client's code default.
173-
174-
This approach maintains compatibility with the established OpenFeature terminology while providing clear semantics through response structure.
94+
- The "ERROR" reason is already used for cases where the flag is not found or misconfigured, so it aligns with the intent of using code defaults.
95+
- This approach avoids introducing new reason codes that would require additional handling in providers and clients.
17596

17697
### API changes
17798

@@ -197,10 +118,14 @@ flags:
197118
198119
#### Single flag evaluation response
199120
121+
A single flag evaluation returns a `404` status code.
122+
200123
```json
201124
{
202125
"key": "my-feature",
203-
"reason": "DEFAULT",
126+
"errorCode": "FLAG_NOT_FOUND",
127+
// Optional error details
128+
"errorDetails": "Targeting not matched, using code default",
204129
"metadata": {}
205130
}
206131
```
@@ -210,59 +135,21 @@ flags:
210135
```json
211136
{
212137
"flags": [
213-
{
214-
"key": "my-feature",
215-
"reason": "DEFAULT",
216-
"metadata": {}
217-
}
138+
// Flag is omitted from bulk response
218139
]
219140
}
220141
```
221142

222-
Note: Both `value` and `variant` fields are intentionally omitted to signal "use code default"
223-
224143
**flagd RPC Response** (ResolveBooleanResponse):
225144

226145
```protobuf
227146
{
228-
"reason": "DEFAULT",
147+
"reason": "ERROR",
148+
"errorCode": "FLAG_NOT_FOUND",
229149
"metadata": {}
230150
}
231151
```
232152

233-
Both the type-specific value field and variant field are omitted
234-
235-
**Provider behavior**:
236-
237-
1. Check response for presence of value field
238-
2. If value field is absent and error code is absent , use code-defined default
239-
3. If value field is present (even if null/false/empty), use that value
240-
4. The variant field will also be absent when delegating to code defaults
241-
5. This logic is consistent across in-process, RPC, and OFREP modes
242-
243-
This approach clearly differentiates between:
244-
245-
- Server returning an actual value with a variant (both fields present)
246-
- Server delegating to code default (both fields absent)
247-
248-
**Example evaluation flow**:
249-
250-
```javascript
251-
// Client code
252-
const value = client.getBooleanValue('my-feature', false, context);
253-
254-
// Server evaluates flag with defaultVariant: null
255-
// No targeting match, so server returns:
256-
{
257-
"reason": "DEFAULT",
258-
"metadata": {}
259-
// Note: both "value" and "variant" fields are omitted
260-
}
261-
262-
// Client detects missing value field and uses its default (false)
263-
// Resolution details show reason: "DEFAULT" with undefined variant
264-
```
265-
266153
### Consequences
267154

268155
- Good, because it eliminates the confusion between code and configuration defaults
@@ -275,43 +162,25 @@ const value = client.getBooleanValue('my-feature', false, context);
275162
- Good, because it maintains full backward compatibility
276163
- Bad, because it requires updates across multiple components (flagd, providers, testbed)
277164
- Bad, because it introduces a new concept that users need to understand
278-
- Neutral, because existing configurations continue to work unchanged
279-
280-
### Remote Evaluation Considerations
281-
282-
This feature works consistently across all flagd evaluation modes through a unified pattern:
283-
284-
1. **In-Process Mode**: Direct evaluation returns responses without value fields when delegating
285-
2. **RPC Mode (gRPC/HTTP)**: Responses omit the type-specific value field to signal delegation
286-
3. **OFREP (OpenFeature Remote Evaluation Protocol)**: HTTP responses omit the value field
287-
288-
The key insight is using field omission to indicate "use code default":
289-
290-
- Present value field (even if null/false/empty) = use this value
291-
- Absent value field = use your code default
292-
- Works identically across all protocols and evaluation modes
293-
- No protocol changes required
165+
- Neutral, because existing configurations continue to work unchange
294166

295167
### Implementation Plan
296168

297169
1. Update flagd-schemas with new JSON schema supporting null default variants
298-
2. Implement core logic in flagd to handle null defaults and omit value/variant fields
299-
3. Update flagd-testbed with comprehensive test cases for all evaluation modes
300-
4. Update OpenFeature providers to handle responses without value fields
301-
5. Documentation updates, migration guides, and proto comment additions
302-
303-
Note: No protobuf schema changes are required, but implementation must carefully handle field omission
170+
2. Update flagd-testbed with comprehensive test cases for all evaluation modes
171+
3. Implement core logic in flagd to handle null defaults and omit value/variant fields
172+
4. Update OpenFeature providers with the latest schema and test harness to ensure they handle the new behavior correctly
173+
5. Documentation updates, migration guides, and playground examples to demonstrate the new configuration options
304174

305175
### Testing Considerations
306176

307177
To ensure correct implementation across all components:
308178

309-
1. **Wire Format Tests**: Verify that protobuf messages with omitted fields are correctly serialized without the fields (not with zero values)
310-
2. **Provider Tests**: Each provider must have tests confirming they detect missing fields correctly in their language
311-
3. **Integration Tests**: End-to-end tests across different language combinations (e.g., Go flagd with Java provider)
312-
4. **OFREP Tests**: Verify JSON responses correctly omit fields (not set to null)
313-
5. **Backward Compatibility Tests**: Ensure old providers handle new responses gracefully
314-
6. **Consistency Tests**: Verify identical behavior across in-process, RPC, and OFREP modes
179+
1. **Provider Tests**: Each component (flagd, providers) must have unit tests verifying the handling of `null` as a default variant
180+
2. **Integration Tests**: End-to-end tests across different language combinations (e.g., Go flagd with Java provider)
181+
3. **OFREP Tests**: Verify JSON responses correctly omits flags with a `null` default variant
182+
4. **Backward Compatibility Tests**: Ensure old providers handle new responses gracefully
183+
5. **Consistency Tests**: Verify identical behavior across in-process, RPC, and OFREP modes
315184

316185
### Open questions
317186

0 commit comments

Comments
 (0)