Skip to content

Hexagonal Architecture

Hexagonal architecture separates business logic (the “core”) from infrastructure details (databases, cloud services, frameworks) via ports (interfaces) and adapters (interchangeable implementations). The core only knows about ports; adapters are wired at composition time (DI).

In Granit, each functional module (BlobStorage, Features, BackgroundJobs, Webhooks, Settings) follows this pattern: a “core” package defines the ports, and separate packages (*.EntityFrameworkCore, *.S3) provide the adapters.

classDiagram
    direction LR

    class IBlobStorage {
        +InitiateUploadAsync()
        +CreateDownloadUrlAsync()
        +DeleteAsync()
    }

    class IBlobDescriptorStoreReader {
        +FindAsync()
    }

    class IBlobDescriptorStoreWriter {
        +SaveAsync()
        +UpdateAsync()
    }

    class IBlobStorageClient {
        +DeleteObjectAsync()
        +HeadObjectAsync()
    }

    class IBlobKeyStrategy {
        +BuildObjectKey()
        +ResolveBucketName()
    }

    class IBlobValidator {
        +ValidateAsync()
    }

    class DefaultBlobStorage {
        core adapter
    }

    class EfBlobDescriptorStore {
        EF Core adapter
    }

    class S3BlobClient {
        S3 adapter
    }

    class PrefixBlobKeyStrategy {
        S3 adapter
    }

    class MagicBytesValidator {
        built-in adapter
    }

    IBlobStorage <|.. DefaultBlobStorage
    DefaultBlobStorage --> IBlobDescriptorStoreReader
    DefaultBlobStorage --> IBlobDescriptorStoreWriter
    DefaultBlobStorage --> IBlobStorageClient
    DefaultBlobStorage --> IBlobKeyStrategy
    DefaultBlobStorage --> IBlobValidator

    IBlobDescriptorStoreReader <|.. EfBlobDescriptorStore
    IBlobDescriptorStoreWriter <|.. EfBlobDescriptorStore
    IBlobStorageClient <|.. S3BlobClient
    IBlobKeyStrategy <|.. PrefixBlobKeyStrategy
    IBlobValidator <|.. MagicBytesValidator
Port (interface)FileAdapter(s)
IBlobStoragesrc/Granit.BlobStorage/IBlobStorage.csDefaultBlobStorage (orchestrator)
IBlobDescriptorStoreReader / IBlobDescriptorStoreWritersrc/Granit.BlobStorage/EfBlobDescriptorStore in Granit.BlobStorage.EntityFrameworkCore
IBlobStorageClientsrc/Granit.BlobStorage/Internal/IBlobStorageClient.csS3BlobClient in Granit.BlobStorage.S3
IBlobKeyStrategysrc/Granit.BlobStorage/IBlobKeyStrategy.csPrefixBlobKeyStrategy in Granit.BlobStorage.S3
IBlobValidatorsrc/Granit.BlobStorage/IBlobValidator.csMagicBytesValidator, MaxSizeValidator (built-in) + custom
ModulePortAdapters
FeaturesIFeatureStoreReader / IFeatureStoreWriterInMemoryFeatureStore, EfCoreFeatureStore
BackgroundJobsIBackgroundJobStoreReader / IBackgroundJobStoreWriterInMemoryBackgroundJobStore, EfBackgroundJobStore
WebhooksIWebhookSubscriptionStoreReader / IWebhookSubscriptionStoreWriterEfWebhookSubscriptionStore
SettingsISettingStoreReader / ISettingStoreWriterEfCoreSettingStore
CachingICacheService(T)DistributedCacheService, HybridCacheService
EncryptionIStringEncryptionProviderAesStringEncryptionProvider
ProblemSolution
Coupling to a cloud provider (S3, Azure Blob)Ports allow swapping adapters without touching the core
Unit tests requiring a databaseInMemoryFeatureStore and InMemoryBackgroundJobStore implement Reader/Writer interfaces, replacing EF Core in tests
ISO 27001 compliance — ability to migrate from S3-compatible storage to a sovereign providerImplementing IBlobStorageClient for the new provider is sufficient
Independent NuGet packagesThe core (Granit.BlobStorage) has no dependency on EF Core or the AWS SDK
// Replacing S3 with MinIO -- only the adapter changes
services.AddSingleton<IBlobStorageClient, MinioBlobClient>();
services.AddSingleton<IBlobKeyStrategy, MinioBlobKeyStrategy>();
// The rest of the application code remains unchanged
IBlobStorage blobStorage = serviceProvider.GetRequiredService<IBlobStorage>();
PresignedUploadTicket ticket = await blobStorage.InitiateUploadAsync(
"medical-documents",
new BlobUploadRequest("mri-report.pdf", "application/pdf", MaxAllowedBytes: 50_000_000),
cancellationToken);