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) File Adapter(s) IBlobStoragesrc/Granit.BlobStorage/IBlobStorage.csDefaultBlobStorage (orchestrator)IBlobDescriptorStoreReader / IBlobDescriptorStoreWritersrc/Granit.BlobStorage/EfBlobDescriptorStore in Granit.BlobStorage.EntityFrameworkCoreIBlobStorageClientsrc/Granit.BlobStorage/Internal/IBlobStorageClient.csS3BlobClient in Granit.BlobStorage.S3IBlobKeyStrategysrc/Granit.BlobStorage/IBlobKeyStrategy.csPrefixBlobKeyStrategy in Granit.BlobStorage.S3IBlobValidatorsrc/Granit.BlobStorage/IBlobValidator.csMagicBytesValidator, MaxSizeValidator (built-in) + custom
Module Port Adapters Features IFeatureStoreReader / IFeatureStoreWriterInMemoryFeatureStore, EfCoreFeatureStoreBackgroundJobs IBackgroundJobStoreReader / IBackgroundJobStoreWriterInMemoryBackgroundJobStore, EfBackgroundJobStoreWebhooks IWebhookSubscriptionStoreReader / IWebhookSubscriptionStoreWriterEfWebhookSubscriptionStoreSettings ISettingStoreReader / ISettingStoreWriterEfCoreSettingStoreCaching ICacheService(T)DistributedCacheService, HybridCacheServiceEncryption IStringEncryptionProviderAesStringEncryptionProvider
Problem Solution Coupling to a cloud provider (S3, Azure Blob) Ports allow swapping adapters without touching the core Unit tests requiring a database InMemoryFeatureStore and InMemoryBackgroundJobStore implement Reader/Writer interfaces, replacing EF Core in testsISO 27001 compliance — ability to migrate from S3-compatible storage to a sovereign provider Implementing IBlobStorageClient for the new provider is sufficient Independent NuGet packages The 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 (
new BlobUploadRequest( " mri-report.pdf " , " application/pdf " , MaxAllowedBytes: 50_000_000 ),