Documentation Index
Fetch the complete documentation index at: https://platform.docs.zenoo.com/llms.txt
Use this file to discover all available pages before exploring further.
Cloud Provider Architecture
The Zenoo Hub’s cloud provider architecture implements a clean separation between business logic and infrastructure through a well-defined abstraction layer. This document provides a technical deep-dive into the architecture, design patterns, and implementation details.
Architecture Overview
The cloud provider abstraction consists of three layers:
┌─────────────────────────────────────────────────────┐
│ Hub Backend │
│ - Workflow Engine │
│ - Component Management │
│ - Business Logic │
│ │
│ Adapters: │
│ ├─ AttributeComponentConfigReaderAdapter │
│ ├─ AttributeComponentConfigStorageAdapter │
│ └─ ApiKeyStoreAdapter │
└────────────────────┬────────────────────────────────┘
│ (depends on interfaces only)
▼
┌─────────────────────────────────────────────────────┐
│ Cloud Provider API │
│ │
│ Interfaces: │
│ ├─ ComponentStore │
│ ├─ SharableStore │
│ ├─ ApiKeyStore │
│ ├─ ApiKeySecretStorage │
│ ├─ ComponentConfigStorage │
│ └─ ComponentConfigReader │
│ │
│ Exceptions: │
│ ├─ CloudProviderException │
│ ├─ StorageException │
│ └─ ApiKeyException │
└────────────────────┬────────────────────────────────┘
│ (implementations)
▼
┌─────────────────────────────────────────────────────┐
│ Cloud Provider AWS │
│ │
│ Sub-modules: │
│ ├─ aws-stores (DynamoDB) │
│ │ ├─ ComponentDynamodbStoreBean │
│ │ ├─ SharableDynamodbStoreBean │
│ │ └─ ApiKeyLookupStoreBean │
│ │ │
│ ├─ aws-secrets (Secrets Manager) │
│ │ ├─ AwsComponentConfigStorage │
│ │ ├─ AwsComponentConfigReader │
│ │ └─ AwsApiKeySecretStorage │
│ │ │
│ ├─ aws-metrics (CloudWatch) │
│ │ └─ AwsMetricPublisher │
│ │ │
│ └─ aws-spring-boot-starter │
│ └─ Auto-configuration │
└─────────────────────────────────────────────────────┘
Module Structure
Hub Domain
Module: hub-domain
Purpose: Contains pure domain models shared across all layers.
Key Classes:
ComponentId - Component identifier (name + revision)
StringComponent - Component definition with DSL
ApiKeySecret - API key with permissions
ApiKeyPermission - Permission definition
ComponentApiKeyLookup - API key lookup data
SharableRecord - Sharable token record
ExecuteRequest - Execution request
ComponentConfigId - Configuration identifier
Dependencies: Minimal
- Jackson (for JSON serialization)
- Lombok (for boilerplate reduction)
- Spring Core (utilities only)
Design Principles:
- No cloud provider dependencies
- No business logic
- Immutable where possible (using records)
- JSON-serializable
Cloud Provider API
Module: cloud-provider-api
Purpose: Defines provider-agnostic interfaces that all cloud implementations must satisfy.
Key Interfaces:
Storage Interfaces
ComponentStore:
public interface ComponentStore {
Mono<StringComponent> get(ComponentId id);
Mono<ComponentId> store(ComponentId id, StringComponent component);
Mono<List<ComponentId>> delete(ComponentId id);
}
SharableStore:
public interface SharableStore {
Mono<SharableRecord> get(String token);
Mono<String> store(SharableRecord record);
Mono<String> delete(String token);
Mono<String> expire(String token);
}
ApiKeyStore:
public interface ApiKeyStore extends ApiKeyReader {
Mono<String> create(ApiKeySecret apiKeySecret);
Mono<String> update(ApiKeySecret apiKeySecret);
Mono<String> delete(String secretName);
}
Configuration Interfaces
ComponentConfigReader (Blocking):
public interface ComponentConfigReader {
String getByKey(ComponentConfigId configId);
}
ComponentConfigStorage (Reactive):
public interface ComponentConfigStorage {
Mono<String> get(ComponentConfigId configId);
Mono<ComponentConfigId> store(ComponentConfigId configId, String json);
Mono<ComponentConfigId> delete(ComponentConfigId configId);
Mono<String> getVersionOfLatestConfig(String configName);
}
Secrets Management
ApiKeySecretStorage:
public interface ApiKeySecretStorage extends ApiKeyReader {
Mono<String> create(ApiKeySecret apiKeySecret);
Mono<String> update(ApiKeySecret apiKeySecret);
Mono<String> delete(String name);
}
Exception Hierarchy:
CloudProviderException
├── StorageException
└── ApiKeyException
Error Types:
public enum ErrorType {
NOT_FOUND, // Resource not found
CONFLICT, // Conflict (e.g., version mismatch)
INVALID_REQUEST, // Invalid input
SERVICE_ERROR // Cloud provider service error
}
Dependencies:
hub-domain (domain models)
- Spring Boot (configuration properties)
- Project Reactor (reactive types)
- Jackson (JSON serialization)
Backend Adapters
Location: backend/src/main/java/com/zenoo/hub/component/{config,security}
Purpose: Bridge between backend’s domain-specific types and cloud provider’s generic types.
Adapter Pattern Implementation
1. AttributeComponentConfigReaderAdapter
Responsibility: Convert JSON strings from cloud provider to backend’s Attribute DSL type.
@Component
@Order(1)
class AttributeComponentConfigReaderAdapter implements ComponentConfigReader {
private final com.zenoo.hub.cloudprovider.config.ComponentConfigReader delegate;
@Override
public Attribute getByKey(ComponentConfigId configId) {
String json = delegate.getByKey(configId);
if (json == null || json.isEmpty()) {
return AttributeBuilder.of();
}
return AttributeBuilder.ofJson(json);
}
}
Key Features:
- Blocking interface for synchronous reads
- Type conversion: String -> Attribute
- Null/empty handling with sensible defaults
@Order(1) for precedence
2. AttributeComponentConfigStorageAdapter
Responsibility: Convert between Attribute and JSON for reactive storage operations.
@Component
class AttributeComponentConfigStorageAdapter implements ComponentConfigStorage {
private final com.zenoo.hub.cloudprovider.config.ComponentConfigStorage delegate;
@Override
public Mono<Attribute> get(ComponentConfigId configId) {
return delegate.get(configId)
.map(AttributeBuilder::ofJson)
.defaultIfEmpty(AttributeBuilder.of())
.onErrorReturn(AttributeBuilder.of());
}
@Override
public Mono<ComponentConfigId> store(ComponentConfigId configId, Attribute config) {
String json = AttributeBuilder.toJson(config);
return delegate.store(configId, json);
}
}
Key Features:
- Reactive interface with Mono/Flux
- Bidirectional type conversion: Attribute
<-> JSON
- Error handling with defaults
- Transparent delegation
3. ApiKeyStoreAdapter
Responsibility: Simple pass-through adapter for API key operations.
@Component
class ApiKeyStoreAdapter implements ApiKeyStore {
private final ApiKeySecretStorage delegate;
@Override
public Mono<ApiKeySecret> get(String key) {
return delegate.get(key);
}
// ... other methods delegate directly
}
Key Features:
- No type conversion needed (uses domain models)
- Pure delegation pattern
- Maintains reactive types
Cloud Provider AWS
Module: cloud-provider-aws (composite module)
Purpose: AWS-specific implementations using DynamoDB, Secrets Manager, and CloudWatch.
Sub-module: aws-stores
Purpose: DynamoDB implementations for storage interfaces.
Key Classes:
ComponentDynamodbStoreBean:
@Service
public class ComponentDynamodbStoreBean extends DynamoDBStoreSupport
implements ComponentStore {
@Override
public Mono<StringComponent> get(ComponentId id) {
return getComponent(id)
.switchIfEmpty(Mono.error(() -> NotFoundException.component(id)))
.onErrorMap(this::wrapAwsException)
.doOnError(error -> log.warn("Component `{}` not found", id, error));
}
private Throwable wrapAwsException(Throwable throwable) {
if (throwable instanceof ResourceNotFoundException) {
return new StorageException(
throwable.getMessage(),
throwable,
CloudProviderException.ErrorType.NOT_FOUND
);
}
// ... other mappings
}
}
Features:
- Exception mapping from AWS SDK to cloud-provider-api
- Retry logic with exponential backoff
- Optimistic locking support
- Operation timeouts
SharableDynamodbStoreBean:
- TTL-based automatic expiration
- High-performance token retrieval
- Configurable expiration
ApiKeyLookupStoreBean:
- Fast component -> API keys lookup
- Exposed function management
- Permission tracking
Sub-module: aws-secrets
Purpose: Secrets Manager implementations for configuration and secrets.
Key Classes:
AwsComponentConfigStorage:
- Version management with stage labels
- Multi-region replication
- Encryption with KMS
AwsApiKeySecretStorage:
- Secure API key storage
- Permission management
- Automatic rotation support
Features:
- JSON serialization with Jackson
- Custom serializers for permissions
- Caching with configurable TTL
- Audit trail support
Sub-module: aws-metrics
Purpose: CloudWatch metrics publishing.
AwsMetricPublisher:
- DynamoDB operation metrics
- Secret access metrics
- Custom dimensions support
Sub-module: aws-spring-boot-starter
Purpose: Spring Boot autoconfiguration for AWS provider.
AwsCloudProviderAutoConfiguration:
@AutoConfiguration
@ConditionalOnClass(DynamoDbAsyncClient.class)
@ConditionalOnProperty(name = "hub.cloud.provider.type",
havingValue = "aws",
matchIfMissing = true)
@Import({
AwsStoresAutoConfiguration.class,
AwsSecretsAutoConfiguration.class,
AwsMetricsAutoConfiguration.class
})
public class AwsCloudProviderAutoConfiguration {
// Configuration beans
}
Features:
- Conditional activation based on classpath and configuration
- Component scanning for AWS beans
- Configuration properties binding
- Defaults to AWS for backward compatibility
Cloud Provider Local
Module: cloud-provider-local (single module)
Purpose: Lightweight, in-memory implementation for local development and testing.
Architecture Philosophy:
- Simplicity over features
- Zero external dependencies
- Fast startup and teardown
- Deterministic behavior for testing
Key Characteristics:
- Single Module: Unlike AWS (4 submodules), local uses a single module for simplicity
- In-Memory Storage: All data stored in
ConcurrentHashMap instances
- No Persistence: Data lost on restart (by design)
- Thread-Safe: Uses concurrent collections and locks where needed
- Reactive: Returns
Mono<T> for API consistency
Storage Implementations
LocalComponentStore:
@Service
public class LocalComponentStore extends LocalStoreSupport
implements ComponentStore {
// Key: "componentName:revision"
private final ConcurrentHashMap<String, StringComponent> components =
new ConcurrentHashMap<>();
// Key: "componentName", Value: "latestRevision"
private final ConcurrentHashMap<String, String> latestRevisions =
new ConcurrentHashMap<>();
@Override
public Mono<StringComponent> get(ComponentId id) {
return wrapWithErrorHandling(
Mono.fromCallable(() -> {
// Resolve LATEST pseudo-revision
ComponentId resolvedId = resolveRevision(id);
StringComponent component = components.get(toKey(resolvedId));
if (component == null) {
throw new NoSuchElementException("Component not found: " + id);
}
return component;
}),
"get component",
id.getName()
);
}
private ComponentId resolveRevision(ComponentId id) {
if ("LATEST".equals(id.getRevision())) {
String latestRevision = latestRevisions.get(id.getName());
if (latestRevision == null) {
throw new NoSuchElementException("No versions found for: " + id.getName());
}
return id.toBuilder().revision(latestRevision).build();
}
return id;
}
}
Features:
- LATEST pseudo-revision resolution (same as AWS)
- Component versioning support
- Optimistic locking for concurrent updates
- Exception mapping to cloud-provider-api types
LocalSharableStore:
@Service
public class LocalSharableStore extends LocalStoreSupport
implements SharableStore {
private final ConcurrentHashMap<String, SharableRecord> sharables =
new ConcurrentHashMap<>();
@Override
public Mono<SharableRecord> get(String token) {
return wrapWithErrorHandling(
Mono.fromCallable(() -> {
SharableRecord record = sharables.get(token);
if (record == null) {
throw new NoSuchElementException("Sharable not found: " + token);
}
// Lazy expiration check
if (isExpired(record)) {
sharables.remove(token);
throw new NoSuchElementException("Sharable expired: " + token);
}
return record;
}),
"get sharable",
token
);
}
public boolean isExpired(SharableRecord record) {
if (record.isExpired()) return true;
if (record.getExpiration() != null &&
Instant.now().isAfter(record.getExpiration())) {
return true;
}
return false;
}
}
Features:
- Lazy expiration (checked on access)
- Scheduled cleanup (optional background task)
- Configurable cleanup interval
LocalApiKeyStore:
@Service
public class LocalApiKeyStore extends LocalStoreSupport
implements ApiKeyStore {
// Key: componentName
private final ConcurrentHashMap<String, ApiKeyRecord> apiKeyRecords =
new ConcurrentHashMap<>();
// ApiKeyRecord uses ReadWriteLock for thread safety
public static class ApiKeyRecord {
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Set<String> secrets = new HashSet<>();
private boolean exposed = false;
private Set<String> exposedFunctions = new HashSet<>();
public void addSecret(String secretName) {
lock.writeLock().lock();
try {
secrets.add(secretName);
} finally {
lock.writeLock().unlock();
}
}
public Set<String> getSecretsOrEmpty() {
lock.readLock().lock();
try {
return new HashSet<>(secrets);
} finally {
lock.readLock().unlock();
}
}
}
}
Features:
- Fine-grained locking with ReadWriteLock
- Fast component -> API keys lookup
- Bidirectional sync with ApiKeySecretStorage
LocalApiKeySecretStorage:
@Service
public class LocalApiKeySecretStorage extends LocalStoreSupport
implements ApiKeySecretStorage {
private final ConcurrentHashMap<String, ApiKeySecret> secrets =
new ConcurrentHashMap<>();
private final LocalApiKeyStore apiKeyStore;
@Override
public Mono<String> create(ApiKeySecret apiKeySecret) {
return wrapWithErrorHandling(
Mono.fromCallable(() -> {
secrets.put(apiKeySecret.name(), apiKeySecret);
return apiKeySecret.name();
})
// Bidirectional sync: update API key store
.flatMap(name -> apiKeyStore.addSecret(apiKeySecret).thenReturn(name)),
"create secret",
apiKeySecret.name()
);
}
}
Features:
- Bidirectional sync with ApiKeyStore
- Permission-based access control
- Secure storage (in-memory)
LocalComponentConfigStorage:
@Service
public class LocalComponentConfigStorage extends LocalStoreSupport
implements ComponentConfigStorage {
// Key: "configKey:version", Value: JSON string
private final ConcurrentHashMap<String, String> configs =
new ConcurrentHashMap<>();
// Key: "configKey", Value: "latestVersion"
private final ConcurrentHashMap<String, String> latestVersions =
new ConcurrentHashMap<>();
}
Features:
- Configuration versioning
- LATEST version resolution
- JSON storage format
Support Classes
LocalStoreSupport (Base Class):
public abstract class LocalStoreSupport {
protected <T> Mono<T> wrapWithErrorHandling(
Mono<T> operation,
String operationName,
String resourceId) {
return operation
.onErrorMap(this::wrapException)
.doOnError(error ->
log.warn("Local store operation '{}' failed for '{}'",
operationName, resourceId, error));
}
protected Throwable wrapException(Throwable throwable) {
if (throwable instanceof IllegalArgumentException) {
return new StorageException(
throwable.getMessage(),
throwable,
CloudProviderException.ErrorType.INVALID_REQUEST
);
}
if (throwable instanceof NoSuchElementException) {
return new StorageException(
throwable.getMessage(),
throwable,
CloudProviderException.ErrorType.NOT_FOUND
);
}
return new StorageException(
throwable.getMessage(),
throwable,
CloudProviderException.ErrorType.SERVICE_ERROR
);
}
}
Features:
- Consistent error handling
- Exception mapping to cloud-provider-api types
- Logging support
SharableCleanupScheduler:
@Component
@ConditionalOnProperty(name = "hub.local.cleanupEnabled",
havingValue = "true",
matchIfMissing = true)
public class SharableCleanupScheduler {
@Autowired
private LocalSharableStore sharableStore;
@Autowired
private LocalProviderConfig config;
private ScheduledExecutorService scheduler;
@PostConstruct
public void start() {
scheduler = Executors.newSingleThreadScheduledExecutor();
long intervalMs = config.getCleanupInterval().toMillis();
scheduler.scheduleAtFixedRate(
() -> {
try {
int removed = sharableStore.cleanupExpired();
if (removed > 0) {
log.info("Cleaned up {} expired sharables", removed);
}
} catch (Exception e) {
log.error("Cleanup task failed", e);
}
},
intervalMs,
intervalMs,
TimeUnit.MILLISECONDS
);
}
@PreDestroy
public void stop() {
if (scheduler != null) {
scheduler.shutdown();
}
}
}
Features:
- Background cleanup task
- Configurable interval
- Graceful shutdown
Auto-Configuration
LocalCloudProviderAutoConfiguration:
@AutoConfiguration
@ConditionalOnProperty(
prefix = "hub.cloud.provider",
name = "type",
havingValue = "local"
)
@ComponentScan(basePackages = {
"com.zenoo.hub.cloudprovider.local.store",
"com.zenoo.hub.cloudprovider.local.secret",
"com.zenoo.hub.cloudprovider.local.config"
})
@EnableConfigurationProperties({
LocalProviderConfig.class,
CloudProviderComponentConfig.class,
CloudProviderSharableConfig.class
})
public class LocalCloudProviderAutoConfiguration {
@Bean
@ConditionalOnProperty(name = "hub.local.cleanupEnabled",
havingValue = "true",
matchIfMissing = true)
public SharableCleanupScheduler sharableCleanupScheduler() {
return new SharableCleanupScheduler();
}
@PostConstruct
public void logActivation() {
log.info("Local Cloud Provider auto-configuration enabled");
log.info("Using in-memory storage for all cloud provider operations");
log.info("Note: All data will be lost on application restart");
}
}
Features:
- Conditional activation via
hub.cloud.provider.type=local
- Component scanning for local provider beans
- Configuration properties binding
- Startup logging for visibility
Testing Utilities
The local provider exposes testing utilities:
// Check storage size
int componentCount = localComponentStore.size();
// Clear storage between tests
localComponentStore.clear();
localSharableStore.clear();
// Manual cleanup
int expiredCount = localSharableStore.cleanupExpired();
// Get all tokens (for testing)
List<String> allTokens = localSharableStore.getAllTokens();
Cloud Provider GCP
Module: cloud-provider-gcp (composite module)
Purpose: GCP-specific implementations using Cloud Firestore (Native Mode), Secret Manager, and Cloud Monitoring.
Sub-module: gcp-stores
Purpose: Firestore implementations for storage interfaces.
Key Classes:
ComponentFirestoreStoreBean:
@Service
public class ComponentFirestoreStoreBean extends FirestoreStoreSupport
implements ComponentStore {
@Override
public Mono<StringComponent> get(ComponentId id) {
return getComponentFromFirestore(id)
.switchIfEmpty(Mono.error(() -> NotFoundException.component(id)))
.onErrorMap(this::wrapFirestoreException)
.doOnError(error -> log.warn("Component `{}` not found", id, error));
}
private Throwable wrapFirestoreException(Throwable throwable) {
if (throwable instanceof com.google.cloud.firestore.FirestoreException) {
return GcpExceptionMapper.map((FirestoreException) throwable);
}
return throwable;
}
}
Features:
- Exception mapping from Firestore SDK to cloud-provider-api
- Retry logic with exponential backoff
- Atomic LATEST pointer updates using transactions
- Operation timeouts
- Composite index auto-creation
SharableFirestoreStoreBean:
- TTL-based automatic expiration (Firestore native TTL)
- High-performance token retrieval
- Configurable expiration
- Native Firestore TTL field support
ApiKeyLookupStoreBean:
- Fast component -> API keys lookup
- Bidirectional mapping support
- Firestore document-based storage
Sub-module: gcp-secrets
Purpose: Secret Manager implementations for configuration and secrets.
Key Classes:
GcpComponentConfigStorage:
- Version management with label-based semantic versioning
- Multi-region replication support
- Automatic encryption at rest
- Caffeine caching with configurable TTL
GcpApiKeySecretStorage:
- Secure API key storage
- Permission management
- JSON serialization with Jackson
- Version limits and automatic cleanup
Features:
- Label-based semantic versioning (workaround for Secret Manager limitations)
- Caching with configurable size and TTL
- Batch operations support
- Custom serializers for permissions
Sub-module: gcp-metrics
Purpose: Cloud Monitoring metrics publishing.
GcpMetricPublisher:
- Firestore operation metrics
- Secret access metrics
- Batch publishing (up to 200 time series per request)
- Level-based filtering (INFO, ERROR, TRACE)
- Custom dimensions support
Sub-module: gcp-spring-boot-starter
Purpose: Spring Boot autoconfiguration for GCP provider.
GcpCloudProviderAutoConfiguration:
@AutoConfiguration
@ConditionalOnClass(Firestore.class)
@ConditionalOnProperty(name = "hub.cloud.provider.type",
havingValue = "gcp")
@Import({
GcpStoresAutoConfiguration.class,
GcpSecretsAutoConfiguration.class,
GcpMetricsAutoConfiguration.class
})
public class GcpCloudProviderAutoConfiguration {
// Configuration beans
@Bean
@ConditionalOnMissingBean
public Firestore firestore(GcpConfig gcpConfig,
GcpFirestoreConfig firestoreConfig) {
GoogleCredentials credentials = resolveCredentials(gcpConfig);
FirestoreOptions options = FirestoreOptions.newBuilder()
.setCredentials(credentials)
.setProjectId(gcpConfig.getProjectId())
.setDatabaseId(firestoreConfig.getDatabase())
.build();
return options.getService();
}
}
Features:
- Conditional activation based on classpath and configuration
- Component scanning for GCP beans
- Configuration properties binding
- Application Default Credentials (ADC) support
- Workload Identity support for GKE
Comparison: AWS vs GCP vs Local
| Aspect | AWS Provider | GCP Provider | Local Provider |
|---|
| Module Structure | 4 submodules | 4 submodules | Single module |
| Storage | DynamoDB | Firestore (Native Mode) | ConcurrentHashMap |
| Secrets | Secrets Manager | Secret Manager | In-memory |
| Persistence | Full | Full | None |
| External Deps | AWS SDK, DynamoDB, Secrets Manager | GCP SDK, Firestore, Secret Manager | None |
| Setup Time | ~2 seconds | ~2 seconds | <100ms |
| Configuration | IAM, credentials, regions | IAM, credentials, project | None required |
| Multi-instance | Yes (via DynamoDB) | Yes (via Firestore) | No (in-memory) |
| Cost | Pay per use | Pay per use | Free |
| Thread Safety | AWS SDK handles | Firestore SDK handles | ConcurrentHashMap + locks |
| Expiration | DynamoDB TTL | Firestore TTL | Lazy + scheduled cleanup |
| Metrics | CloudWatch | Cloud Monitoring | Logging only |
| Versioning | Stage labels | Label-based semantic versioning | In-memory tracking |
| Multi-region | Global Tables | Automatic replication | No |
| Best For | Production on AWS | Production on GCP | Development, testing |
Design Trade-offs
Why single module instead of splitting?
- Simplicity: Easier to understand and maintain
- No submodules needed (no external services to abstract)
- Faster compile times
- Smaller dependency graph
Why ConcurrentHashMap?
- Built-in thread safety
- Good performance for concurrent access
- No external dependencies
- Familiar to Java developers
Why no persistence?
- Keeps implementation simple
- Forces proper test isolation
- Encourages proper CI/CD practices
- Faster test execution
Why scheduled cleanup instead of automatic expiration?
- JVM has no built-in TTL mechanism
- Scheduled task is simple and effective
- Lazy cleanup reduces CPU overhead
- Configurable for different use cases
Design Patterns
1. Adapter Pattern
Used By: Backend adapters
Purpose: Convert between backend’s domain types and cloud provider’s generic types.
Example:
Backend (Attribute) <-> Adapter <-> Cloud Provider (String/JSON)
Benefits:
- Clean separation of concerns
- Type safety in backend
- Provider independence
2. Strategy Pattern
Used By: Cloud provider selection
Purpose: Select cloud provider implementation at runtime.
Example:
hub:
cloud:
provider:
type: aws # Strategy selection
Benefits:
- Runtime provider switching
- Easy testing with different providers
- Future extensibility
3. Repository Pattern
Used By: Storage interfaces
Purpose: Abstract data persistence from business logic.
Benefits:
- Clean data access layer
- Testability with mocks
- Provider-agnostic business logic
4. Facade Pattern
Used By: ComponentConfigServiceFacade
Purpose: Provide unified interface to multiple config readers.
Benefits:
- Single entry point for configuration
- Multiple reader support
- Precedence management
Exception Handling
Exception Flow
AWS SDK Exception
|
DynamoDBStoreSupport.wrapAwsException()
|
CloudProviderException (with ErrorType)
|
Backend catches and handles
Exception Mapping
| AWS Exception | CloudProviderException Type | ErrorType |
|---|
| ResourceNotFoundException | StorageException | NOT_FOUND |
| ConditionalCheckFailedException | StorageException | CONFLICT |
| ValidationException | StorageException | INVALID_REQUEST |
| ProvisionedThroughputExceededException | StorageException | SERVICE_ERROR |
| AwsServiceException | StorageException | SERVICE_ERROR |
Retry Logic
Implementation: DynamoDBStoreSupport
protected <T> Mono<T> applyRetry(Mono<T> mono, String request, String partitionKey) {
return mono.timeout(dynamodbTimeout)
.doOnError(e -> log.warn("Error during dynamodb request: {} {}", request, partitionKey, e))
.retryWhen(dynamodbRetry
.doBeforeRetry(retry -> log.debug("Retrying DynamoDB request {} {}", request, partitionKey)));
}
private static boolean isRetryableError(Throwable e) {
return (e instanceof TimeoutException)
|| RETRYABLE_EXCEPTIONS.contains(e.getClass())
|| ((e instanceof AwsServiceException) && isRetryableStatusCode(awsServiceException));
}
Configuration:
hub:
aws:
dynamodb:
retryStrategy:
requestTimeout: 500ms
maxRetries: 10
backoff: 100ms
Reactive Programming Model
Why Reactive?
- Non-blocking I/O - Better resource utilization
- Backpressure - Handle slow consumers
- Composability - Chain operations easily
- Error handling - Propagate errors through pipeline
Reactive Types
Mono<T>: Represents 0 or 1 element
Mono<StringComponent> get(ComponentId id);
Flux<T>: Represents 0 to N elements
Flux<ComponentId> deleteByPartitionKey(String name);
Common Operators
Transformation:
.map(json -> AttributeBuilder.ofJson(json))
Error Handling:
.onErrorMap(this::wrapAwsException)
.defaultIfEmpty(AttributeBuilder.of())
.onErrorReturn(AttributeBuilder.of())
Composition:
.flatMap(component -> deleteRelated(component))
.then(deleteComponent())
Dependency Injection Flow
Bean Registration Order
-
Cloud Provider Beans (AWS)
- Registered by
AwsStoresAutoConfiguration
ComponentDynamodbStoreBean, AwsApiKeySecretStorage, etc.
-
Adapter Beans (Backend)
- Registered by component scanning
AttributeComponentConfigReaderAdapter, etc.
- Autowired with cloud provider implementations
-
Service Beans (Backend)
ComponentConfigServiceFacade, ApiKeyServiceBean, etc.
- Autowired with adapters
Example Injection Chain
ComponentConfigServiceFacade
| (injects List<ComponentConfigReader>)
AttributeComponentConfigReaderAdapter
| (injects cloud provider reader)
AwsComponentConfigReader
| (injects AWS SDK client)
SecretsManagerClient
Testing Strategy
Unit Tests
Adapter Tests:
- Mock cloud provider delegates
- Test type conversions
- Test error handling
Cloud Provider Tests:
- Mock AWS SDK clients
- Test exception mapping
- Test retry logic
Integration Tests
LocalStack:
- Full AWS service emulation
- End-to-end testing
- No cloud costs
Test Containers:
- Embedded DynamoDB
- Isolated test environment
Example Test Structure
@Test
void shouldConvertJsonToAttribute() {
// Given
var delegate = Mock(ComponentConfigReader.class);
var adapter = new AttributeComponentConfigReaderAdapter(delegate);
var json = '{"key": "value"}';
// When
when(delegate.getByKey(any())).thenReturn(json);
var result = adapter.getByKey(configId);
// Then
assertThat(result.key).isEqualTo("value");
}
Connection Pooling
- AWS SDK manages connection pools automatically
- Default: 50 connections per client
- Tune for high-throughput scenarios
Caching
Secrets Manager:
hub:
aws:
secrets:
cache: true
cacheTtl: 300s
Benefits:
- Reduced API calls
- Lower latency
- Cost savings
Trade-offs:
- Stale data for up to cacheTtl
- Memory usage
Batch Operations
- Use DynamoDB
BatchGetItem for multiple reads
- Use
BatchWriteItem for multiple writes
- Up to 25 items per batch
Security Considerations
Principle of Least Privilege
- Grant minimum required IAM permissions
- Use resource-based policies where possible
- Separate read/write permissions
Encryption
At Rest:
- DynamoDB: Default encryption with AWS-managed keys
- Secrets Manager: Automatic encryption with KMS
In Transit:
- All AWS API calls use HTTPS
- TLS 1.2+ required
Audit Logging
- CloudTrail logs all API calls
- CloudWatch Logs for application logs
- Structured logging for correlation
Future Extensibility
Adding New Providers
To add a new provider (e.g., Azure):
- Create module
cloud-provider-azure
- Implement interfaces from
cloud-provider-api
- Map provider exceptions to
CloudProviderException
- Create Spring Boot autoconfiguration
- Add tests
Supporting New Storage Types
To add new storage interfaces:
- Define interface in
cloud-provider-api
- Add domain models to
hub-domain
- Implement in cloud provider modules
- Create adapter in backend (if type conversion needed)
- Update autoconfiguration
See Also