Skip to content

Hexagonal Architecture — Ports & Adapters

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
CachingIFusionCacheFusionCache (via Granit.Caching)
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);