Skip to main content

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

AspectAWS ProviderGCP ProviderLocal Provider
Module Structure4 submodules4 submodulesSingle module
StorageDynamoDBFirestore (Native Mode)ConcurrentHashMap
SecretsSecrets ManagerSecret ManagerIn-memory
PersistenceFullFullNone
External DepsAWS SDK, DynamoDB, Secrets ManagerGCP SDK, Firestore, Secret ManagerNone
Setup Time~2 seconds~2 seconds<100ms
ConfigurationIAM, credentials, regionsIAM, credentials, projectNone required
Multi-instanceYes (via DynamoDB)Yes (via Firestore)No (in-memory)
CostPay per usePay per useFree
Thread SafetyAWS SDK handlesFirestore SDK handlesConcurrentHashMap + locks
ExpirationDynamoDB TTLFirestore TTLLazy + scheduled cleanup
MetricsCloudWatchCloud MonitoringLogging only
VersioningStage labelsLabel-based semantic versioningIn-memory tracking
Multi-regionGlobal TablesAutomatic replicationNo
Best ForProduction on AWSProduction on GCPDevelopment, 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 ExceptionCloudProviderException TypeErrorType
ResourceNotFoundExceptionStorageExceptionNOT_FOUND
ConditionalCheckFailedExceptionStorageExceptionCONFLICT
ValidationExceptionStorageExceptionINVALID_REQUEST
ProvisionedThroughputExceededExceptionStorageExceptionSERVICE_ERROR
AwsServiceExceptionStorageExceptionSERVICE_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

  1. Cloud Provider Beans (AWS)
    • Registered by AwsStoresAutoConfiguration
    • ComponentDynamodbStoreBean, AwsApiKeySecretStorage, etc.
  2. Adapter Beans (Backend)
    • Registered by component scanning
    • AttributeComponentConfigReaderAdapter, etc.
    • Autowired with cloud provider implementations
  3. 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");
}

Performance Considerations

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):
  1. Create module cloud-provider-azure
  2. Implement interfaces from cloud-provider-api
  3. Map provider exceptions to CloudProviderException
  4. Create Spring Boot autoconfiguration
  5. Add tests

Supporting New Storage Types

To add new storage interfaces:
  1. Define interface in cloud-provider-api
  2. Add domain models to hub-domain
  3. Implement in cloud provider modules
  4. Create adapter in backend (if type conversion needed)
  5. Update autoconfiguration

See Also