Skip to content

Commit fa6955a

Browse files
refactor!: java materialization interface rework (#170)
Co-authored-by: Nicklas Lundin <nicklasl@spotify.com>
1 parent 6c26181 commit fa6955a

23 files changed

+805
-843
lines changed

openfeature-provider/java/README.md

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -152,28 +152,32 @@ This is particularly useful for:
152152
- **Production customization**: Custom TLS settings, proxies, or connection pooling
153153
- **Debugging**: Add custom logging or tracing interceptors
154154

155-
## Sticky Assignments
155+
## Materializations
156156

157-
The provider supports **Sticky Assignments** for consistent variant assignments across flag evaluations. This ensures users receive the same variant even when their targeting attributes change, and enables pausing experiment intake.
157+
The provider supports **materializations** for two key use cases:
158158

159+
1. **Sticky Assignments**: Maintain consistent variant assignments across evaluations even when targeting attributes change. This enables pausing intake (stopping new users from entering an experiment) while keeping existing users in their assigned variants.
159160
**📖 See the [Integration Guide: Sticky Assignments](../INTEGRATION_GUIDE.md#sticky-assignments)** for how sticky assignments work and their benefits.
160161

161-
**By default, sticky assignments are managed by Confidence servers.** When sticky assignment data is needed, the provider makes a network call to Confidence, which maintains the sticky repository server-side with automatic 90-day TTL management. This requires no additional setup.
162+
1. **Custom Targeting via Materialized Segments**: Efficiently target precomputed sets of identifiers from datasets. Instead of evaluating complex targeting rules at runtime, materializations allow for fast lookups of whether a unit (user, session, etc.) is included in a target segment.
163+
164+
**By default, materializations are managed by Confidence servers.** When sticky assignment data is needed, the provider makes a network call to Confidence, which maintains the sticky repository server-side with automatic 90-day TTL management. This requires no additional setup.
162165

163166
### Custom Materialization Storage
164167

165-
Optionally, you can implement a custom `MaterializationRepository` to manage sticky assignments in your own storage (Redis, database, etc.) to eliminate network calls and improve latency:
168+
Optionally, you can implement a custom `MaterializationStore` to manage materialization data in your own storage (Redis, database, etc.) to eliminate network calls and improve latency:
166169

167170
```java
168-
// Optional: Custom storage for sticky assignments
169-
MaterializationRepository repository = new RedisMaterializationRepository(jedisPool, "myapp");
171+
// Optional: Custom storage for materialization data
172+
MaterializationStore store = new RedisMaterializationStore(jedisPool);
170173
OpenFeatureLocalResolveProvider provider = new OpenFeatureLocalResolveProvider(
171174
clientSecret,
172-
repository
175+
store
173176
);
174177
```
175178

176179
For detailed information on how to implement custom storage backends, see [STICKY_RESOLVE.md](STICKY_RESOLVE.md).
180+
See the `InMemoryMaterializationStoreExample` class in the test sources for a reference implementation, or review the `MaterializationStore` javadoc for detailed API documentation.
177181

178182
## Requirements
179183

openfeature-provider/java/STICKY_RESOLVE.md

Lines changed: 79 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,129 @@
1-
# Sticky Resolve Documentation
1+
# Materialization Store Documentation
22

33
## Overview
44

5-
Sticky Resolve ensures users receive the same variant throughout an experiment, even if their targeting attributes change or you pause new assignments.
5+
The Materialization Store provides persistent storage for flag resolution data, supporting two key use cases:
66

7-
**Two main use cases:**
8-
1. **Consistent experience** - User moves countries but keeps the same variant
9-
2. **Pause intake** - Stop new assignments while maintaining existing ones
7+
1. **Sticky Assignments** - Maintain consistent variant assignments across evaluations even when targeting attributes change. This enables pausing intake (stopping new users from entering an experiment) while keeping existing users in their assigned variants.
108

11-
**Default behavior:** Sticky assignments are managed by Confidence servers with automatic 90-day TTL. When needed, the provider makes a network call to Confidence. No setup required.
9+
2. **Custom Targeting via Materialized Segments** - Precomputed sets of identifiers from datasets that should be targeted. Instead of evaluating complex targeting rules at runtime, materializations allow efficient lookup of whether a unit (user, session, etc.) is included in a target segment.
10+
11+
**Default behavior:** Materializations are managed by Confidence servers with automatic 90-day TTL. When needed, the provider makes a network call to Confidence. No setup required.
1212

1313
## How It Works
1414

15-
### Default: Server-Side Storage (RemoteResolverFallback)
15+
### Default: Server-Side Storage (UnsupportedMaterializationStore)
1616

1717
**Flow:**
1818
1. Local WASM resolver attempts to resolve
19-
2. If sticky data needed → network call to Confidence
20-
3. Confidence checks its sticky repository, returns variant
21-
4. Assignment stored server-side with 90-day TTL (auto-renewed on access)
19+
2. If materialization data needed → `UnsupportedMaterializationStore` throws exception
20+
3. Provider falls back to remote gRPC resolution via Confidence
21+
4. Confidence checks its materialization repository, returns variant/inclusion data
22+
5. Data stored server-side with 90-day TTL (auto-renewed on access)
2223

2324
**Server-side configuration (in Confidence UI):**
2425
- Optionally skip targeting criteria for sticky assignments
2526
- Pause/resume new entity intake
2627
- Automatic TTL management
2728

28-
### Custom: Local Storage (MaterializationRepository)
29+
### Custom: Local Storage (MaterializationStore)
2930

30-
Implement `MaterializationRepository` to store assignments locally and eliminate network calls.
31+
Implement `MaterializationStore` to store materialization data locally and eliminate network calls.
3132

3233
**Interface:**
3334
```java
34-
public interface MaterializationRepository extends StickyResolveStrategy {
35-
// Load assignments for a unit (e.g., user ID)
36-
CompletableFuture<Map<String, MaterializationInfo>> loadMaterializedAssignmentsForUnit(
37-
String unit, String materialization);
38-
39-
// Store new assignments
40-
CompletableFuture<Void> storeAssignment(
41-
String unit, Map<String, MaterializationInfo> assignments);
35+
public interface MaterializationStore {
36+
// Batch read of materialization data
37+
CompletionStage<List<ReadResult>> read(List<? extends ReadOp> ops);
38+
39+
// Batch write of materialization data (optional)
40+
default CompletionStage<Void> write(Set<? extends WriteOp> ops) {
41+
throw new UnsupportedOperationException("Unimplemented method 'write'");
42+
}
43+
}
44+
```
45+
46+
**Key Concepts:**
47+
- **Materialization** - Identifier for a materialization context (experiment, flag, or materialized segment)
48+
- **Unit** - Entity identifier (user ID, session ID, etc.)
49+
- **Rule** - Targeting rule identifier within a flag
50+
- **Variant** - Assigned variant name for the unit+rule combination
51+
52+
**Operation Types:**
53+
54+
Read operations (`ReadOp`):
55+
```java
56+
// Check if unit is in materialized segment
57+
sealed interface ReadOp {
58+
record Inclusion(String materialization, String unit) implements ReadOp {}
59+
record Variant(String materialization, String unit, String rule) implements ReadOp {}
60+
}
61+
```
62+
63+
Read results (`ReadResult`):
64+
```java
65+
sealed interface ReadResult {
66+
// Result for segment membership check
67+
record Inclusion(String materialization, String unit, boolean included) implements ReadResult {}
68+
69+
// Result for sticky variant assignment
70+
record Variant(String materialization, String unit, String rule, Optional<String> variant)
71+
implements ReadResult {}
4272
}
4373
```
4474

45-
**MaterializationInfo structure:**
75+
Write operations (`WriteOp`):
4676
```java
47-
record MaterializationInfo(
48-
boolean isUnitInMaterialization,
49-
Map<String, String> ruleToVariant // rule ID -> variant name
50-
)
77+
sealed interface WriteOp {
78+
// Store sticky variant assignment
79+
record Variant(String materialization, String unit, String rule, String variant)
80+
implements WriteOp {}
81+
}
5182
```
5283

5384
## Implementation Examples
5485

55-
### In-Memory (Testing/Development)
86+
### In-Memory (Testing/Development Only)
87+
88+
**Warning: Do not use in-memory storage in production.** In-memory implementations lose all materialization data on restart, breaking sticky assignments and materialized segments. Production systems require persistent storage (Redis, database, etc.).
5689

57-
[Here is an example](src/test/java/com/spotify/confidence/InMemoryMaterializationRepoExample.java) on how to implement a simple in-memory `MaterializationRepository`. The same approach can be used with other more persistent storages (like Redis or similar) which is highly recommended for production use cases.
90+
See `InMemoryMaterializationStoreExample` for a reference implementation. Use this pattern with persistent storage backends for production.
5891

5992
#### Usage
6093

6194
```java
62-
MaterializationRepository repository = new InMemoryMaterializationRepoExample();
95+
MaterializationStore store = new InMemoryMaterializationStore();
6396

6497
OpenFeatureLocalResolveProvider provider = new OpenFeatureLocalResolveProvider(
6598
clientSecret,
66-
repository
99+
store
67100
);
68101
```
69102

70103
## Best Practices
71104

72-
1. **Fail gracefully** - Storage errors shouldn't fail flag resolution
73-
2. **Use 90-day TTL** - Match Confidence's default behavior, renew on read
74-
3. **Connection pooling** - Use pools for Redis/DB connections
75-
4. **Monitor metrics** - Track cache hit rate, storage latency, errors
76-
5. **Test both paths** - Missing assignments (cold start) and existing assignments
105+
1. **Thread-safe implementation** - Implementations must be thread-safe for concurrent flag resolution
106+
2. **Fail gracefully** - Storage errors shouldn't fail flag resolution; throw `MaterializationNotSupportedException` to trigger remote fallback
107+
3. **Use 90-day TTL** - Match Confidence's default behavior, renew on read
108+
4. **Idempotent writes** - Write operations should be idempotent
109+
5. **Batch operations** - Both `read` and `write` accept batches for efficiency
110+
6. **Connection pooling** - Use pools for Redis/DB connections
111+
7. **Monitor metrics** - Track cache hit rate, storage latency, errors
112+
8. **Test both paths** - Missing assignments (cold start) and existing assignments
77113

78114
## When to Use Custom Storage
79115

80-
| Strategy | Best For | Trade-offs |
116+
| Implementation | Best For | Trade-offs |
81117
|----------|----------|------------|
82-
| **RemoteResolverFallback** (default) | Most apps | Simple, managed by Confidence. Network calls when needed. |
83-
| **MaterializationRepository** (in-memory) | Single-instance apps, testing | Fast, no network. Lost on restart. |
84-
| **MaterializationRepository** (Redis/DB) | Distributed/Multi instance apps | No network calls. Requires storage infra. |
118+
| **UnsupportedMaterializationStore** (default) | Most apps | Simple, managed by Confidence. Network calls when needed. |
119+
| **MaterializationStore** (in-memory) | Testing/development only | Fast, no network. **Lost on restart - do not use in production.** |
120+
| **MaterializationStore** (Redis/DB) | Production apps needing local storage | No network calls. Requires persistent storage infrastructure. |
121+
122+
**Start with the default.** Only implement custom storage with persistent backends (Redis, database) if you need to eliminate network calls or work offline.
123+
124+
## Error Handling
85125

86-
**Start with the default.** Only implement custom storage if you need to eliminate network calls or work offline.
126+
When implementing `read()`, throw `MaterializationNotSupportedException` to trigger fallback to remote gRPC resolution. This allows graceful degradation when local storage is unavailable.
87127

88128
## Additional Resources
89129

openfeature-provider/java/src/main/java/com/spotify/confidence/ConfidenceGrpcFlagResolver.java

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,18 @@
11
package com.spotify.confidence;
22

3-
import com.google.protobuf.Struct;
43
import com.spotify.confidence.flags.resolver.v1.FlagResolverServiceGrpc;
54
import com.spotify.confidence.flags.resolver.v1.ResolveFlagsRequest;
65
import com.spotify.confidence.flags.resolver.v1.ResolveFlagsResponse;
7-
import com.spotify.confidence.flags.resolver.v1.Sdk;
8-
import com.spotify.confidence.flags.resolver.v1.Sdk.Builder;
9-
import com.spotify.confidence.flags.resolver.v1.SdkId;
106
import io.grpc.ManagedChannel;
11-
import java.util.List;
127
import java.util.concurrent.CompletableFuture;
138
import java.util.concurrent.TimeUnit;
149

1510
/**
1611
* A simplified gRPC-based flag resolver for fallback scenarios in the local provider. This is a
1712
* copy of the core functionality from GrpcFlagResolver adapted for the local provider's needs.
1813
*/
19-
public class ConfidenceGrpcFlagResolver {
14+
public class ConfidenceGrpcFlagResolver implements RemoteResolver {
2015
private final ManagedChannel channel;
21-
private final Builder sdkBuilder =
22-
Sdk.newBuilder().setVersion("0.2.8"); // Using static version for local provider
2316

2417
private final FlagResolverServiceGrpc.FlagResolverServiceFutureStub stub;
2518

@@ -28,20 +21,13 @@ public ConfidenceGrpcFlagResolver(ChannelFactory channelFactory) {
2821
this.stub = FlagResolverServiceGrpc.newFutureStub(channel);
2922
}
3023

31-
public CompletableFuture<ResolveFlagsResponse> resolve(
32-
List<String> flags, String clientSecret, Struct context) {
24+
@Override
25+
public CompletableFuture<ResolveFlagsResponse> resolve(ResolveFlagsRequest request) {
3326
return GrpcUtil.toCompletableFuture(
34-
stub.withDeadlineAfter(10_000, TimeUnit.MILLISECONDS)
35-
.resolveFlags(
36-
ResolveFlagsRequest.newBuilder()
37-
.setClientSecret(clientSecret)
38-
.addAllFlags(flags)
39-
.setEvaluationContext(context)
40-
.setSdk(sdkBuilder.setId(SdkId.SDK_ID_JAVA_PROVIDER).build())
41-
.setApply(true)
42-
.build()));
27+
stub.withDeadlineAfter(10_000, TimeUnit.MILLISECONDS).resolveFlags(request));
4328
}
4429

30+
@Override
4531
public void close() {
4632
channel.shutdownNow();
4733
}

openfeature-provider/java/src/main/java/com/spotify/confidence/MaterializationInfo.java

Lines changed: 0 additions & 14 deletions
This file was deleted.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.spotify.confidence;
2+
3+
/**
4+
* Thrown when a {@link MaterializationStore} doesn't support the requested operation.
5+
*
6+
* <p>This exception triggers the provider to fall back to remote gRPC resolution via the Confidence
7+
* service, which manages materializations server-side.
8+
*
9+
* <p>Users typically don't need to catch this exception - it's handled internally by the provider's
10+
* resolution logic.
11+
*
12+
* @see UnsupportedMaterializationStore
13+
* @see MaterializationStore
14+
*/
15+
public class MaterializationNotSupportedException extends RuntimeException {}

openfeature-provider/java/src/main/java/com/spotify/confidence/MaterializationRepository.java

Lines changed: 0 additions & 12 deletions
This file was deleted.

0 commit comments

Comments
 (0)