Skip to content

Layered Architecture

Layered architecture organizes code into strictly hierarchical levels of responsibility. Each layer depends only on the layer below it, never on the layer above. In Granit, three main layers structure each module:

  • Domain: entities, marker interfaces, events, business exceptions
  • Application: orchestration services, checkers, managers
  • Infrastructure: EF Core persistence, S3 clients, Vault, Redis cache
flowchart BT
    subgraph Infrastructure
        EF["*.EntityFrameworkCore<br/>(DbContext, EF stores)"]
        S3["*.S3<br/>(S3BlobClient)"]
        VAULT["Granit.Vault<br/>(VaultClientFactory)"]
        CACHE["Granit.Caching<br/>(DistributedCacheService)"]
    end

    subgraph Application
        BM["BackgroundJobManager"]
        FC["FeatureChecker"]
        BS["DefaultBlobStorage"]
        EH["GranitExceptionHandler"]
    end

    subgraph Domain
        ENT["Entity, AuditedEntity<br/>FullAuditedEntity"]
        INT["IBlobStorage, IFeatureStore<br/>IBackgroundJobStore"]
        EVT["IDomainEvent<br/>IIntegrationEvent"]
        EXC["BusinessException<br/>NotFoundException"]
        MRK["ISoftDeletable, IMultiTenant<br/>IActive"]
    end

    EF -->|implements| INT
    S3 -->|implements| INT
    VAULT -->|used by| Application
    CACHE -->|used by| Application

    BM -->|uses| INT
    FC -->|uses| INT
    BS -->|uses| INT

    Application -->|manipulates| ENT
    Application -->|publishes| EVT
    Application -->|throws| EXC

    style Domain fill:#2d5a27,color:#fff
    style Application fill:#4a9eff,color:#fff
    style Infrastructure fill:#ff6b6b,color:#fff
ComponentFile
Entity > CreationAuditedEntity > AuditedEntity > FullAuditedEntitysrc/Granit.Core/Domain/
ISoftDeletable, IMultiTenant, IActivesrc/Granit.Core/Domain/
IDomainEvent, IIntegrationEventsrc/Granit.Core/Events/
BusinessException, NotFoundException, ConflictExceptionsrc/Granit.Core/Exceptions/
ICurrentTenant, NullTenantContextsrc/Granit.Core/MultiTenancy/
ServiceFile
FeatureCheckersrc/Granit.Features/Checker/FeatureChecker.cs
BackgroundJobManagersrc/Granit.BackgroundJobs/Internal/BackgroundJobManager.cs
DefaultBlobStoragesrc/Granit.BlobStorage/Internal/DefaultBlobStorage.cs
PermissionCheckersrc/Granit.Authorization/Services/PermissionChecker.cs
GranitExceptionHandlersrc/Granit.ExceptionHandling/GranitExceptionHandler.cs

Infrastructure layer (*.EntityFrameworkCore, *.S3, etc.)

Section titled “Infrastructure layer (*.EntityFrameworkCore, *.S3, etc.)”
ComponentFile
EfBlobDescriptorStoresrc/Granit.BlobStorage.EntityFrameworkCore/Internal/EfBlobDescriptorStore.cs
EfCoreFeatureStoresrc/Granit.Features.EntityFrameworkCore/Internal/EfCoreFeatureStore.cs
EfBackgroundJobStoresrc/Granit.BackgroundJobs.EntityFrameworkCore/Internal/EfBackgroundJobStore.cs
S3BlobClientsrc/Granit.BlobStorage.S3/Internal/S3BlobClient.cs
VaultClientFactorysrc/Granit.Vault/Services/VaultClientFactory.cs
Infrastructure --> Application --> Domain
Never in the reverse direction

*.EntityFrameworkCore packages reference the core package (e.g., Granit.BlobStorage) but never the other way around. The core package contains no dependency on EF Core or the AWS SDK.

ProblemSolution
Coupling between business logic and databaseThe domain only knows about interfaces (ports)
Difficulty testing business logic in isolationApplication services are testable with mocks
Changing provider (S3 to another) impacts the entire codebaseOnly the infrastructure layer changes
EF Core entities leaking into API DTOsStrict separation prevents shortcuts
// Entity hierarchy -- Domain layer
public sealed class Patient : FullAuditedEntity, IMultiTenant
{
public Guid? TenantId { get; set; }
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public DateOnly BirthDate { get; set; }
}
// FullAuditedEntity automatically provides:
// - Id (Guid, sequential via IGuidGenerator)
// - CreatedAt, CreatedBy (ISO 27001 audit -- creation)
// - ModifiedAt, ModifiedBy (ISO 27001 audit -- modification)
// - IsDeleted, DeletedAt, DeletedBy (GDPR soft delete)
// - TenantId (multi-tenant isolation via IMultiTenant)