RFC-040: Multi-Language Client SDK Architecture
Status: Draft Author: Platform Team Created: 2025-10-19 Updated: 2025-10-19
Abstract
This RFC defines the architecture for Prism client SDKs in Rust, Python, and Go. These SDKs provide first-class, idiomatic client libraries that expose pattern interfaces (Producer, Consumer, KeyValue) directly to application developers. The SDKs are designed with:
- Full integration test coverage using testcontainers and real local backends
- Shared directory structure across all three languages for consistency
- Async-first APIs leveraging native concurrency primitives (tokio, asyncio, goroutines)
- Direct gRPC communication with Prism proxy for maximum performance
- OAuth2 authentication as the default auth mechanism
- Namespace-aware configuration for multi-tenancy support
The SDKs abstract away the complexity of backend implementation while providing simple, type-safe APIs that feel native to each language ecosystem.
Motivation
Problem Statement
Application teams currently face several challenges when integrating with Prism:
- No Official Client Libraries: Teams must hand-write gRPC clients or use generic tools
- Pattern Discovery: No clear mapping from use case → pattern API
- Auth Complexity: OAuth2 token management is repetitive and error-prone
- Namespace Configuration: Unclear how to configure pattern-specific settings
- Testing Difficulty: No guidance on testing against Prism locally
- Language Inconsistency: Each team invents their own client patterns
Goals
- Developer Experience: Simple, idiomatic APIs in Rust, Python, and Go
- Type Safety: Leverage protobuf code generation for compile-time safety
- Pattern-First Design: Expose Producer, Consumer, KeyValue as first-class APIs
- OAuth2 Integration: Built-in token acquisition and refresh
- Comprehensive Testing: Full integration test suites with testcontainers
- Namespace Support: Easy configuration of namespace-specific settings
- Observability: Built-in metrics, tracing, and structured logging
- Cross-Language Consistency: Same API shape across Rust, Python, Go
Non-Goals
- Admin API Client: This RFC focuses on data plane clients (separate admin SDK)
- Reactive Streams: Advanced backpressure mechanisms (use native streams)
- Custom Serialization: Only protobuf-based serialization supported initially
- Multi-Region Failover: Automatic cross-region failover (use DNS-based discovery)
- Schema Registry Integration: Schema evolution (deferred to separate RFC)
Design Principles
1. Pattern-Centric APIs
Expose patterns as top-level modules, not low-level gRPC calls:
// ✅ Good: Pattern-centric
use prism_client::Producer;
let producer = Producer::connect("orders-namespace").await?;
producer.publish(b"order-123", order_data).await?;
// ❌ Bad: Low-level gRPC
use prism_proto::pubsub::PubSubBasicInterfaceClient;
let mut client = PubSubBasicInterfaceClient::connect("http://prism:8980").await?;
client.publish(PublishRequest { topic: "...", ... }).await?;
2. Synchronous and Asynchronous APIs
Synchronous APIs as the default for simplicity, with asynchronous APIs available for advanced use cases:
- Sync: Simple, blocking APIs for CLI tools, scripts, and traditional applications
- Async: High-performance APIs for concurrent workloads using native runtimes
Each language provides both sync and async interfaces:
- Rust: Sync wrappers around
tokio(usingRuntime::block_on) - Python: Sync module alongside
asyncio(usingasyncio.runinternally) - Go: Naturally synchronous with goroutines (async patterns via channels)
Default recommendation: Use synchronous APIs unless you have specific concurrency requirements.
3. Pluggable Authentication with Credential Providers
SDKs support multiple authentication methods through a Credential Provider abstraction:
# client-config.yaml
# Option 1: API Key (simplest, for development/internal services)
auth:
type: api_key
api_key: ${PRISM_API_KEY}
# Option 2: OAuth2 (production, token management)
auth:
type: oauth2
token_endpoint: https://auth.example.com/token
client_id: my-app
client_secret: ${CLIENT_SECRET}
scopes: [prism.producer, prism.consumer]
# Option 3: Mutual TLS (certificate-based)
auth:
type: mtls
client_cert: /etc/prism/client.pem
client_key: /etc/prism/client-key.pem
# Option 4: Environment variable (for local dev)
auth:
type: env
token_var: PRISM_TOKEN
Credential Provider Trait (Rust):
pub trait CredentialProvider {
async fn get_credentials(&self) -> Result<Credentials>;
}
pub enum Credentials {
Bearer(String), // OAuth2 token or API key
ApiKey(String), // Simple API key
Mtls(ClientCert), // Client certificate
Custom(HashMap<String, String>), // Custom headers
}
// Built-in providers
impl CredentialProvider for OAuth2Provider { /* ... */ }
impl CredentialProvider for ApiKeyProvider { /* ... */ }
impl CredentialProvider for EnvProvider { /* ... */ }
impl CredentialProvider for MtlsProvider { /* ... */ }
OAuth2-specific handling (when using OAuth2 provider):
- Token acquisition on first request
- Token refresh before expiration
- Retry with new token on 401 responses
- Token caching with encryption
4. Full Integration Test Coverage
Every SDK must include:
- Unit tests: Mock gRPC responses for isolated logic
- Integration tests: Real Prism proxy + backends via testcontainers
- End-to-end tests: Multi-pattern workflows (produce → consume)
- Performance tests: Throughput and latency benchmarks
Target coverage:
- Producer/Consumer: 85%+ line coverage
- KeyValue: 85%+ line coverage
- Auth: 90%+ line coverage
- Config: 90%+ line coverage
5. Shared Directory Structure
All SDKs follow the same directory layout for discoverability:
prism-client-{lang}/
├── src/ # Source code
│ ├── patterns/ # Pattern implementations
│ │ ├── producer.{ext}
│ │ ├── consumer.{ext}
│ │ └── keyvalue.{ext}
│ ├── auth/ # OAuth2 client
│ ├── config/ # Configuration management
│ ├── proto/ # Generated protobuf code
│ └── client.{ext} # Main client entry point
├── tests/ # Test suite
│ ├── unit/ # Unit tests
│ ├── integration/ # Integration tests (testcontainers)
│ └── e2e/ # End-to-end tests
├── examples/ # Usage examples
└── docs/ # SDK-specific documentation
Client SDK Architecture
High-Level Components
┌────────────────────────────────────────────────────────────┐
│ Application Code │
│ │
│ producer.publish("order-123", data) │
│ message = consumer.receive() │
│ kv.set("user:42", user_data) │
└────────────────────────┬───────────────────────────────────┘
│
│ Pattern APIs
│
┌────────────────────────▼────────────────────────────── ─────┐
│ Prism Client SDK │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Producer │ │ Consumer │ │ KeyValue │ │
│ │ │ │ │ │ │ │
│ │ - publish() │ │ - receive() │ │ - get() │ │
│ │ - flush() │ │ - ack() │ │ - set() │ │
│ │ │ │ - nack() │ │ - delete() │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ OAuth2 │ │ Config │ │ Metrics │ │
│ │ Client │ │ Manager │ │ Tracer │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└────────────────────────┬───────────────────────────────────┘
│
│ gRPC + OAuth2 headers
│
┌────────────────────────▼───────────────────────────────────┐
│ Prism Proxy │
│ │
│ Pattern Layer → Backend Drivers → Backends │
└────────────────────────────────────────────────────────────┘
Core Abstractions
0. Synchronous API Design
To maximize developer ergonomics, all SDKs provide synchronous wrappers as the primary interface, with async APIs available as an advanced option.
Rust Synchronous API:
// Synchronous (recommended for most use cases)
use prism_client::Client;
fn main() -> Result<()> {
let client = Client::connect("localhost:8980")?;
let producer = client.producer("orders")?;
producer.publish("new-orders", b"order-123")?;
client.close()?;
Ok(())
}
// Async API (for high-performance servers)
use prism_client::r#async::Client as AsyncClient;
#[tokio::main]
async fn main() -> Result<()> {
let client = AsyncClient::connect("localhost:8980").await?;
let producer = client.producer("orders").await?;
producer.publish("new-orders", b"order-123").await?;
Ok(())
}
Python Synchronous API:
# Synchronous (recommended for Django, Flask, scripts)
from prism_client.sync import Client
with Client.from_file("prism.yaml") as client:
producer = client.producer("orders")
producer.publish("new-orders", b"order-123")
# Async API (for FastAPI, aiohttp)
from prism_client import Client
async with Client.from_file("prism.yaml") as client:
producer = await client.producer("orders")
await producer.publish("new-orders", b"order-123")
Go API (naturally synchronous):
// Go is synchronous by default
client, err := prism.Connect("localhost:8980")
defer client.Close()
producer := client.Producer("orders")
producer.Publish(ctx, "new-orders", []byte("order-123"))
Implementation Strategy:
The sync wrapper maintains an internal async runtime and blocks on async operations:
// Rust sync wrapper (internal implementation)
pub struct Client {
async_client: AsyncClient,
runtime: Arc<Runtime>,
}
impl Client {
pub fn producer(&self, namespace: &str) -> Result<Producer> {
self.runtime.block_on(self.async_client.producer(namespace))
}
}
# Python sync wrapper (internal implementation)
class Client:
def producer(self, namespace: str) -> Producer:
return asyncio.run(self._async_client.producer(namespace))
Benefits:
- Lower barrier to entry: No async knowledge required for basic use
- Smaller conceptual surface area: 80% of users avoid async complexity
- Framework compatibility: Works with Django, Flask, CLI tools
- No code duplication: Sync wraps async implementation