This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This is the Java client for the Orisun Event Store - a gRPC-based event sourcing system. The client provides a type-safe, intuitive interface for interacting with the Orisun server, supporting event storage, retrieval, streaming subscriptions, and administrative operations.
The project includes two main clients:
- OrisunClient - For event store operations (save, read, subscribe to events)
- AdminClient - For user management and administrative operations
./gradlew build # Build and test the project
./gradlew jar # Build JAR only
./gradlew shadowJar # Build fat JAR with dependencies
./gradlew clean # Clean build artifacts./gradlew test # Run all tests
./gradlew test --tests "*ClassName" # Run specific test classThe project uses proto definitions from a git submodule in protos/:
git submodule update --init --recursive # Initialize submodule
git submodule update --remote protos # Update proto files
./gradlew build # Regenerate Java classes from proto./gradlew publishToMavenLocal # Publish to local Maven cache
./gradlew publish # Publish to remote repositoryThe OrisunClient class uses a builder pattern (OrisunClient.newBuilder()) with the following key components:
Core Client (OrisunClient.java)
- Main entry point for all operations
- Implements both synchronous and asynchronous methods
- Uses gRPC blocking and non-blocking stubs
- Implements
AutoCloseablefor proper resource cleanup - Supports single-server, multi-server, DNS-based, and static-based load balancing
Authentication Flow
TokenCache- Manages authentication tokens, extracting tokens from response headers (x-auth-token) and caching them for reuse- Falls back to Basic Authentication when no token is cached
- Authentication is applied via gRPC interceptors that inject metadata into requests
Request Validation (RequestValidator.java)
- Validates all requests before sending to server
- Checks for required fields, UUID formats, and logical constraints
- Throws
OrisunExceptionwith detailed context for validation failures
Error Handling
OrisunException- Base exception with context map for debuggingOptimisticConcurrencyException- Specific exception for version conflicts (expected vs actual version)
Event Subscription (EventSubscription.java)
- Manages streaming subscriptions to events
- Uses
EventHandlerinterface for callbacks (onEvent, onError, onCompleted) - Implements
AutoCloseablefor cleanup - Handles authentication for streaming connections
Logging (Logger.java, DefaultLogger.java)
- Pluggable logging interface
- Default logger with configurable log levels (INFO, WARN, ERROR, DEBUG)
- Can be customized via builder pattern
The AdminClient class mirrors the architecture of OrisunClient but focuses on administrative operations:
Core Admin Client (AdminClient.java)
- Entry point for user management and admin operations
- Implements synchronous methods for all admin RPCs
- Uses gRPC blocking stubs for all operations
- Implements
AutoCloseablefor proper resource cleanup - Supports the same connection strategies as
OrisunClient
Admin Request Validation (AdminRequestValidator.java)
- Validates admin-specific requests before sending to server
- Checks for password requirements (minimum 8 characters)
- Validates UUID formats for user IDs
- Ensures new passwords differ from current passwords
- Throws
OrisunExceptionwith detailed context for validation failures
Supported Admin Operations:
- User Management:
createUser,deleteUser,changePassword,listUsers - Authentication:
validateCredentials - Statistics:
getUserCount,getEventCount
Index management is exposed on OrisunClient through the EventStore service.
Both clients share the same authentication flow, token caching mechanism, and connection management patterns.
The client uses generated gRPC code from Protocol Buffer definitions:
- Proto files are in
protos/directory (submodule) - Generated Java classes are in
build/generated/source/proto/ - Uses grpc-netty-shaded for networking (includes Netty in the JAR)
- Supports multiple connection strategies:
- Single server:
withServer("localhost", 5005) - Multiple servers:
withServer(host1, port1).withServer(host2, port2) - DNS-based:
withDnsTarget("dns:///example.com:5005") - Static targets:
withStaticTarget("static:///host1:5005,host2:5005")
- Single server:
- Configurable keep-alive settings to maintain connections
- Load balancing policies (default: round_robin)
Tests use in-process gRPC servers with mock implementations (OrisunClientTest.java):
- Uses
ServerBuilder.forPort(0)for dynamic port allocation MockEventStoreServiceimplements the gRPC service interface- Tests cover sync operations, async operations, subscriptions, and error cases
- All tests use ephemeral ports to avoid conflicts
- Builder Pattern - Used extensively in
OrisunClient.Builderfor configuration - Interceptor Pattern - gRPC interceptors inject authentication and handle token extraction
- Observer Pattern -
StreamObserverfor async operations and event subscriptions - Validation Pattern -
RequestValidatorperforms pre-request validation - Resource Management - Both
OrisunClientandEventSubscriptionimplementAutoCloseable
- Token Caching: The
TokenCacheautomatically extracts tokens from response headers and reuses them for subsequent requests, reducing authentication overhead - Concurrency: Uses
AtomicReferencefor thread-safe token caching - Metadata Handling: Authentication metadata is copied carefully through interceptor chains to avoid loss
- Resource Cleanup: Always use try-with-resources or explicitly call
close()on both client and subscription objects - Error Context: Exceptions include context maps with operation names, boundaries, and other debugging information
- gRPC 1.75.0 (protobuf, stub, netty-shaded, testing, inprocess)
- Protobuf 4.28.2
- JUnit 5.10.1 for testing
- javax.annotation-api 1.3.2
Creating a client:
try (OrisunClient client = OrisunClient.newBuilder()
.withServer("localhost", 5005)
.withBasicAuth("username", "password")
.withTimeout(30)
.build()) {
// Use client
}Saving events:
Eventstore.SaveEventsRequest request = Eventstore.SaveEventsRequest.newBuilder()
.setBoundary("boundary-name")
.addEvents(Eventstore.EventToSave.newBuilder()
.setEventId(UUID.randomUUID().toString())
.setEventType("EventType")
.setData("{\"data\":\"value\"}")
.build())
.build();
Eventstore.WriteResult result = client.saveEvents(request);Subscribing to events:
EventSubscription subscription = client.subscribeToEvents(request,
new EventSubscription.EventHandler() {
public void onEvent(Eventstore.Event event) { /* handle */ }
public void onError(Throwable error) { /* handle */ }
public void onCompleted() { /* handle */ }
});
// Later: subscription.close();Creating an admin client:
try (AdminClient adminClient = AdminClient.newBuilder()
.withServer("localhost", 5005)
.withBasicAuth("admin", "adminpassword")
.withTimeout(30)
.build()) {
// Use admin client
}Creating a user:
CreateUserRequest request = CreateUserRequest.newBuilder()
.setName("John Doe")
.setUsername("johndoe")
.setPassword("securePassword123")
.addRoles("user")
.addRoles("admin")
.build();
AdminUser user = adminClient.createUser(request);Listing users:
List<AdminUser> users = adminClient.listUsers();
for (AdminUser user : users) {
System.out.println("User: " + user.getUsername());
}Validating credentials:
ValidateCredentialsRequest request = ValidateCredentialsRequest.newBuilder()
.setUsername("johndoe")
.setPassword("securePassword123")
.build();
ValidateCredentialsResponse response = adminClient.validateCredentials(request);
if (response.getSuccess()) {
System.out.println("Valid credentials for: " + response.getUser().getName());
}Getting statistics:
long userCount = adminClient.getUserCount();
long eventCount = adminClient.getEventCount("users-boundary");Creating an index:
CreateIndexRequest request = CreateIndexRequest.newBuilder()
.setBoundary("events-boundary")
.setName("idx_user_id")
.addFields(IndexField.newBuilder()
.setJsonKey("userId")
.setValueType(ValueType.TEXT)
.build())
.addFields(IndexField.newBuilder()
.setJsonKey("timestamp")
.setValueType(ValueType.TIMESTAMPTZ)
.build())
.addConditions(IndexCondition.newBuilder()
.setKey("eventType")
.setOperator("=")
.setValue("UserCreated")
.build())
.setConditionCombinator(ConditionCombinator.AND)
.build();
orisunClient.createIndex(request);Dropping an index:
DropIndexRequest request = DropIndexRequest.newBuilder()
.setBoundary("events-boundary")
.setName("idx_user_id")
.build();
orisunClient.dropIndex(request);