Skip to content

Commit 62e080d

Browse files
aepfliclaude
andcommitted
refactor: Standardize builders and make POJOs immutable
Major improvements to API consistency and immutability: ## Builder Pattern Standardization - Unified all builder class names to use `Builder` instead of class-specific names - Updated references across codebase (ImmutableMetadata, ProviderEvaluation, etc.) - Fixed compilation errors in Telemetry.java after builder renaming ## POJO Immutability Improvements - **ProviderEvaluation**: Made immutable with final fields, private constructors, removed all setters - **FlagEvaluationDetails**: Made immutable with final fields, private constructors, removed all setters - **EventDetails**: Made constructor private, standardized on builder-only pattern - **ProviderEventDetails**: Made constructors private, standardized on builder-only pattern ## Code Quality Fixes - Fixed checkstyle violations by adding missing Javadoc comments - Fixed SpotBugs issue with defensive copying in ProviderEventDetails.Builder - Added comprehensive API improvement roadmap in API_IMPROVEMENTS.md ## Breaking Changes - Public constructors removed from POJOs - use builders instead - Public setters removed from evaluation classes - objects are now immutable - Some tests will need updates to use builder pattern instead of constructors This enforces immutability and consistent builder patterns across the API, improving thread safety and API usability. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> Signed-off-by: Simon Schrottner <[email protected]>
1 parent 473a981 commit 62e080d

File tree

11 files changed

+351
-138
lines changed

11 files changed

+351
-138
lines changed

API_IMPROVEMENTS.md

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
# OpenFeature Java API Improvements
2+
3+
This document outlines improvement opportunities for the OpenFeature Java API based on analysis of the current codebase structure.
4+
5+
## Status
6+
7+
**Completed**: Builder class names have been standardized to use `Builder` instead of class-specific names (e.g., `ImmutableMetadataBuilder``Builder`)
8+
9+
## 1. POJO Structure Consistency
10+
11+
### Current Issues
12+
- **Mixed Mutability Patterns**: `ProviderEvaluation` is mutable with both builder pattern AND public setters, while event details are immutable with builders only
13+
- **Constructor Overloading vs Builders**: Some classes provide multiple constructors alongside builders
14+
15+
### Improvement Prompts
16+
```
17+
Make ProviderEvaluation immutable by:
18+
1. Making all fields final
19+
2. Removing public setters (setValue, setVariant, etc.)
20+
3. Adding private constructor that takes all parameters
21+
4. Ensuring builder is the only way to create instances
22+
23+
Remove constructor overloads from POJOs and standardize on builder pattern only for:
24+
- FlagEvaluationDetails
25+
- ProviderEvaluation
26+
- EventDetails
27+
- ProviderEventDetails
28+
```
29+
30+
## 2. Builder Pattern Standardization
31+
32+
### Current Issues
33+
- **Inconsistent Method Names**: Some builders use `addX()` (ImmutableMetadata), others use `x()`
34+
- **Validation Inconsistency**: Some builders validate in `build()`, others in setter methods
35+
36+
### Improvement Prompts
37+
```
38+
Update ImmutableMetadata.Builder to use consistent naming:
39+
- Change addString() → string()
40+
- Change addInteger() → integer()
41+
- Change addLong() → longValue()
42+
- Change addFloat() → floatValue()
43+
- Change addDouble() → doubleValue()
44+
- Change addBoolean() → boolean()
45+
46+
Move all validation logic to build() methods instead of setter methods for:
47+
- All builder classes that currently validate in setters
48+
- Add comprehensive validation in build() with clear error messages
49+
50+
Ensure all builder methods return 'this' for fluent interface consistency
51+
```
52+
53+
## 3. Structure Handling Unification
54+
55+
### Current Issues
56+
- **Inconsistent Context Constructors**: `ImmutableContext` and `MutableContext` have different constructor patterns
57+
- **Value Conversion Duplication**: `convertValue()` method exists in multiple places
58+
59+
### Improvement Prompts
60+
```
61+
Create unified context creation patterns:
62+
1. Add static factory methods: Context.of(), Context.empty(), Context.withTargeting(key)
63+
2. Standardize constructor patterns between ImmutableContext and MutableContext
64+
3. Add builder() static methods to both context types for consistency
65+
66+
Extract value conversion logic:
67+
1. Create ValueConverter utility class
68+
2. Move convertValue() from Structure interface to utility
69+
3. Update all implementations to use centralized conversion logic
70+
71+
Add convenience methods for common structure operations:
72+
- Structure.empty()
73+
- Structure.of(Map<String, Object>)
74+
- Structure.withAttribute(key, value)
75+
```
76+
77+
## 4. Metadata Handling Improvements
78+
79+
### Current Issues
80+
- **Interface Hierarchy**: `ClientMetadata` has deprecated `getName()` method
81+
- **Builder Inconsistency**: `ImmutableMetadata` builder uses `addType()` methods vs standard patterns
82+
83+
### Improvement Prompts
84+
```
85+
Clean up metadata interfaces:
86+
1. Remove deprecated getName() method from ClientMetadata after checking usage
87+
2. Create clear separation between ClientMetadata and generic Metadata
88+
3. Consider if both interfaces are needed or can be unified
89+
90+
Add metadata factory methods:
91+
- Metadata.empty()
92+
- Metadata.of(String name)
93+
- Metadata.builder() shortcuts for common cases
94+
95+
Improve metadata builder ergonomics:
96+
- Add putAll(Map<String, Object>) method
97+
- Add convenience methods for common metadata patterns
98+
```
99+
100+
## 5. Event Details Architecture Refinement
101+
102+
### Current Issues
103+
- **Complex Composition**: `EventDetails` composes `ProviderEventDetails` but both implement same interface
104+
105+
### Improvement Prompts
106+
```
107+
Evaluate event details architecture:
108+
1. Consider if separate ProviderEventDetails and EventDetails are necessary
109+
2. Document the relationship and usage patterns clearly
110+
3. If keeping both, ensure clear distinction in naming and purpose
111+
112+
Add event details convenience methods:
113+
- EventDetails.forProviderError(String providerName, ErrorCode code, String message)
114+
- EventDetails.forProviderReady(String providerName)
115+
- EventDetails.forConfigurationChange(String providerName, List<String> flagsChanged)
116+
117+
Improve event details validation:
118+
- Ensure providerName is always required per OpenFeature spec
119+
- Add validation in builder.build() methods
120+
- Provide clear error messages for invalid states
121+
```
122+
123+
## 6. API Ergonomics and Developer Experience
124+
125+
### High Priority Improvements
126+
```
127+
Add static import friendly factory methods:
128+
- Value.of(Object) for common value creation
129+
- Context.of(String targetingKey) for simple contexts
130+
- Context.of(String targetingKey, Map<String, Object> attributes)
131+
132+
Add null safety annotations:
133+
- @Nullable for optional parameters and return values
134+
- @NonNull for required parameters
135+
- Import javax.annotation or create custom annotations
136+
137+
Create fluent shortcuts for common patterns:
138+
- EvaluationContext.withTargeting(String key)
139+
- FlagEvaluationDetails.success(String flagKey, T value)
140+
- FlagEvaluationDetails.error(String flagKey, T defaultValue, ErrorCode code, String message)
141+
```
142+
143+
### Medium Priority Improvements
144+
```
145+
Reduce method overloading where builders can be used:
146+
- Evaluate if multiple overloaded constructors are needed
147+
- Prefer builder pattern over method overloads for complex objects
148+
149+
Improve error messages and validation:
150+
- Add descriptive error messages in builder validation
151+
- Include parameter names and expected values in exceptions
152+
- Add @throws documentation for checked exceptions
153+
154+
Consider Optional usage for nullable returns:
155+
- Evaluate using Optional<T> instead of null returns
156+
- Focus on public API methods that commonly return null
157+
- Document null-safety contracts clearly
158+
```
159+
160+
### Low Priority Improvements
161+
```
162+
Package structure optimization:
163+
- Consider if all API classes need to be in single package
164+
- Evaluate creating sub-packages for: contexts, events, metadata, evaluation
165+
- Maintain backward compatibility during any restructuring
166+
167+
Documentation improvements:
168+
- Add usage examples in class-level Javadocs
169+
- Include common patterns and anti-patterns
170+
- Add code examples for complex builder usage
171+
172+
Performance optimizations:
173+
- Reduce object allocation in hot paths (evaluation)
174+
- Consider object pooling for frequently created objects
175+
- Optimize map operations in context merging
176+
```
177+
178+
## 7. Checkstyle Fixes Needed
179+
180+
Based on current checkstyle errors:
181+
182+
```
183+
Fix checkstyle issues in:
184+
1. ProviderEventDetails.java:19 - Add empty line before @deprecated tag
185+
2. FlagEvaluationDetails.java:4 - Remove unused Optional import
186+
3. EventDetails.java - Add Javadoc comments for missing builder methods:
187+
- flagsChanged() method
188+
- message() method
189+
- eventMetadata() method
190+
- errorCode() method
191+
```
192+
193+
## Implementation Guidelines
194+
195+
### Breaking Changes
196+
- Document all breaking changes with migration guides
197+
- Consider deprecation periods for public API changes
198+
- Provide automated migration tools where possible
199+
200+
### Backward Compatibility
201+
- Maintain existing public API surface where possible
202+
- Use @Deprecated annotations with clear migration paths
203+
- Version new features appropriately
204+
205+
### Testing Strategy
206+
- Add comprehensive tests for all builder patterns
207+
- Test null safety and validation thoroughly
208+
- Include integration tests for common usage patterns
209+
- Maintain test coverage above 80% for all changes
210+
211+
## Next Steps
212+
213+
1. **Fix Checkstyle Issues** - Address the 6 current checkstyle violations
214+
2. **Prioritize High-Value Changes** - Start with POJO consistency and builder standardization
215+
3. **Create Migration Guide** - Document any breaking changes for users
216+
4. **Update Documentation** - Refresh examples and usage patterns
217+
5. **Performance Testing** - Ensure changes don't negatively impact performance
218+
219+
This document serves as a roadmap for incrementally improving the API while maintaining stability and backward compatibility.

openfeature-api/src/main/java/dev/openfeature/api/EvaluationEvent.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ public void setAttributes(Map<String, Object> attributes) {
3737
this.attributes = attributes != null ? new HashMap<>(attributes) : new HashMap<>();
3838
}
3939

40-
public static EvaluationEventBuilder builder() {
41-
return new EvaluationEventBuilder();
40+
public static Builder builder() {
41+
return new Builder();
4242
}
4343

4444
@Override
@@ -66,21 +66,21 @@ public String toString() {
6666
/**
6767
* Builder class for creating instances of EvaluationEvent.
6868
*/
69-
public static class EvaluationEventBuilder {
69+
public static class Builder {
7070
private String name;
7171
private Map<String, Object> attributes = new HashMap<>();
7272

73-
public EvaluationEventBuilder name(String name) {
73+
public Builder name(String name) {
7474
this.name = name;
7575
return this;
7676
}
7777

78-
public EvaluationEventBuilder attributes(Map<String, Object> attributes) {
78+
public Builder attributes(Map<String, Object> attributes) {
7979
this.attributes = attributes != null ? new HashMap<>(attributes) : new HashMap<>();
8080
return this;
8181
}
8282

83-
public EvaluationEventBuilder attribute(String key, Object value) {
83+
public Builder attribute(String key, Object value) {
8484
this.attributes.put(key, value);
8585
return this;
8686
}

openfeature-api/src/main/java/dev/openfeature/api/EventDetails.java

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public class EventDetails implements EventDetailsInterface {
2525
* @param domain the domain associated with this event (may be null)
2626
* @param providerEventDetails the provider event details (required)
2727
*/
28-
public EventDetails(String providerName, String domain, ProviderEventDetails providerEventDetails) {
28+
private EventDetails(String providerName, String domain, ProviderEventDetails providerEventDetails) {
2929
this.providerName =
3030
Objects.requireNonNull(providerName, "providerName is required by OpenFeature specification");
3131
this.domain = domain;
@@ -71,16 +71,16 @@ public ErrorCode getErrorCode() {
7171
return providerEventDetails.getErrorCode();
7272
}
7373

74-
public static EventDetailsBuilder builder() {
75-
return new EventDetailsBuilder();
74+
public static Builder builder() {
75+
return new Builder();
7676
}
7777

7878
/**
7979
* Returns a builder initialized with the current state of this object.
8080
*
8181
* @return a builder for EventDetails
8282
*/
83-
public EventDetailsBuilder toBuilder() {
83+
public Builder toBuilder() {
8484
return builder()
8585
.providerName(this.providerName)
8686
.domain(this.domain)
@@ -117,30 +117,36 @@ public String toString() {
117117
/**
118118
* Builder class for creating instances of EventDetails.
119119
*/
120-
public static class EventDetailsBuilder {
120+
public static class Builder {
121121
private String providerName;
122122
private String domain;
123123
private ProviderEventDetails providerEventDetails;
124124

125-
private EventDetailsBuilder() {}
125+
private Builder() {}
126126

127-
public EventDetailsBuilder providerName(String providerName) {
127+
public Builder providerName(String providerName) {
128128
this.providerName = providerName;
129129
return this;
130130
}
131131

132-
public EventDetailsBuilder domain(String domain) {
132+
public Builder domain(String domain) {
133133
this.domain = domain;
134134
return this;
135135
}
136136

137-
public EventDetailsBuilder providerEventDetails(ProviderEventDetails providerEventDetails) {
137+
public Builder providerEventDetails(ProviderEventDetails providerEventDetails) {
138138
this.providerEventDetails = providerEventDetails;
139139
return this;
140140
}
141141

142142
// Convenience methods for building provider event details inline
143-
public EventDetailsBuilder flagsChanged(List<String> flagsChanged) {
143+
/**
144+
* Sets the list of flags that changed.
145+
*
146+
* @param flagsChanged list of flag keys that changed
147+
* @return this builder
148+
*/
149+
public Builder flagsChanged(List<String> flagsChanged) {
144150
ensureProviderEventDetailsBuilder();
145151
this.providerEventDetails = ProviderEventDetails.builder()
146152
.flagsChanged(flagsChanged)
@@ -151,7 +157,13 @@ public EventDetailsBuilder flagsChanged(List<String> flagsChanged) {
151157
return this;
152158
}
153159

154-
public EventDetailsBuilder message(String message) {
160+
/**
161+
* Sets the message describing the event.
162+
*
163+
* @param message message describing the event (should be populated for PROVIDER_ERROR events)
164+
* @return this builder
165+
*/
166+
public Builder message(String message) {
155167
ensureProviderEventDetailsBuilder();
156168
this.providerEventDetails = ProviderEventDetails.builder()
157169
.flagsChanged(getProviderEventDetailsOrEmpty().getFlagsChanged())
@@ -162,7 +174,13 @@ public EventDetailsBuilder message(String message) {
162174
return this;
163175
}
164176

165-
public EventDetailsBuilder eventMetadata(ImmutableMetadata eventMetadata) {
177+
/**
178+
* Sets the metadata associated with the event.
179+
*
180+
* @param eventMetadata metadata associated with the event
181+
* @return this builder
182+
*/
183+
public Builder eventMetadata(ImmutableMetadata eventMetadata) {
166184
ensureProviderEventDetailsBuilder();
167185
this.providerEventDetails = ProviderEventDetails.builder()
168186
.flagsChanged(getProviderEventDetailsOrEmpty().getFlagsChanged())
@@ -173,7 +191,13 @@ public EventDetailsBuilder eventMetadata(ImmutableMetadata eventMetadata) {
173191
return this;
174192
}
175193

176-
public EventDetailsBuilder errorCode(ErrorCode errorCode) {
194+
/**
195+
* Sets the error code for the event.
196+
*
197+
* @param errorCode error code (should be populated for PROVIDER_ERROR events)
198+
* @return this builder
199+
*/
200+
public Builder errorCode(ErrorCode errorCode) {
177201
ensureProviderEventDetailsBuilder();
178202
this.providerEventDetails = ProviderEventDetails.builder()
179203
.flagsChanged(getProviderEventDetailsOrEmpty().getFlagsChanged())
@@ -208,5 +232,4 @@ public EventDetails build() {
208232
return new EventDetails(providerName, domain, providerEventDetails);
209233
}
210234
}
211-
212235
}

0 commit comments

Comments
 (0)