Skip to content

Framework vs Modules — Classification & Dependency Rules

When a codebase reaches 210 packages in a flat directory, it becomes unclear which packages are foundational infrastructure and which are optional business features. Without a formal boundary:

  • A “utility” package quietly gains a dependency on a business module, coupling the core to a feature.
  • Removing or replacing a module becomes impossible because framework packages depend on it.
  • New contributors cannot tell which packages they must understand versus which they can skip.

Frameworks like ABP solve this with separate repositories (framework/ vs modules/). Granit solves it with a classification rule, an architecture test, and solution filters — no physical split required.

A package is a module if its name (after stripping Granit.) matches one of 17 registered module roots. Everything else under src/Granit.* (except bundles) is framework.

One sentence, one test, zero ambiguity.

AxisFrameworkModule
Abstraction levelLow-level, genericHigh-level, domain-specific
ReusabilityAny application typeSpecific business need
Change frequencyRare (stability contract)Frequent (feature evolution)
Business knowledgeZero — no domain conceptsFull — owns the domain model

A framework package must never contain business-domain types. If Granit.Caching starts defining OrderCachePolicy, it must be refactored or moved to a module.

The 17 registered module roots, grouped by domain:

RootPackagesDescription
DataExchange7Import/export pipeline (CSV, Excel, Wolverine)
DocumentGeneration3PDF and Excel document rendering
ReferenceData3Lookup data management
Templating6Scriban template engine, workflow integration
Timeline5Event timeline with notifications
Workflow5State machine workflows
RootPackagesDescription
BackgroundJobs4Recurring and delayed job scheduling
BlobStorage10File storage (S3, Azure, GCS, local)
Imaging3Image processing (MagickNet)
Webhooks4Outbound webhook management
RootPackagesDescription
Bff5Backend for Frontend (YARP proxy)
Identity9User identity management
OpenIddict5OpenID Connect server
RootPackagesDescription
Auditing4Audit trail and configuration change tracking
Http.Cookies3Cookie consent (GDPR, Klaro)
RootPackagesDescription
Notifications28Multi-channel notifications (Email, SMS, Push, WebPush, SignalR)
RootPackagesDescription
Oidc2OpenID Connect client utilities

Everything that is not a module root (and not a bundle) is framework:

FamilyPackagesRole
Granit (core)1Module system, domain types, shared abstractions
Timing, Guids, Diagnostics3Zero-dependency primitives
Validation5FluentValidation infrastructure + regional rules
Persistence5EF Core 10, migrations, DB providers
Caching2Distributed cache (FusionCache + Redis)
Http.*10API docs, versioning, CORS, resilience, output caching
Security, Encryption, Vault6Encryption, secret management (4 cloud providers)
Authentication7JWT, DPoP, API keys (4 IdP providers)
Authorization3RBAC engine, EF persistence, management endpoints
Wolverine3Message bus + DB providers
Events2Domain and distributed event bus
Localization4i18n engine, EF persistence, source generator
Settings, Features6Runtime settings and feature flags
QueryEngine3Generic filtering, sorting, paging
MultiTenancy1Tenant resolution and context
Privacy4GDPR tooling, erasure, anonymization
AI8AI abstractions, providers, extraction, vector data
Observability, Testing3OpenTelemetry, test utilities
Analyzers2Roslyn analyzers and code fixes
RateLimiting1Rate limiting with feature flag integration
flowchart TD
    classDef fw fill:#0ea5e9,stroke:#0284c7,color:#fff
    classDef mod fill:#e879f9,stroke:#c026d3,color:#fff
    classDef bridge fill:#f59e0b,stroke:#b45309,color:#fff

    subgraph Framework
        CORE["Core"]:::fw
        INFRA["Persistence, Caching,\nWolverine, Events"]:::fw
        SEC["Auth, AuthZ,\nVault, Encryption"]:::fw
        PLAT["Http.*, Validation,\nLocalization, Settings"]:::fw
    end

    subgraph Modules
        BIZ["Workflow, Timeline,\nDataExchange, Templating"]:::mod
        STORE["BlobStorage, Imaging,\nBackgroundJobs, Webhooks"]:::mod
        IDENT["Identity, OpenIddict,\nBff, Notifications"]:::mod
        COMP["Auditing, Http.Cookies"]:::mod
    end

    CORE --> INFRA
    CORE --> SEC
    CORE --> PLAT
    INFRA --> BIZ
    INFRA --> STORE
    SEC --> IDENT
    PLAT --> COMP

The arrow always points from framework to module. A module depends on the framework. A framework package never imports a module.

Some framework packages need optional integration with a module. Instead of adding a direct dependency, Granit uses bridge sub-packages:

Bridge packageFramework parentModule dependency
Granit.Privacy.BackgroundJobsGranit.PrivacyGranit.BackgroundJobs
Granit.Privacy.NotificationsGranit.PrivacyGranit.Notifications

The base framework package (Granit.Privacy) stays independent. The bridge is opt-in: applications that use both Privacy and BackgroundJobs add Granit.Privacy.BackgroundJobs explicitly.

The architecture test exempts bridges automatically: a framework project Granit.X.{ModuleRoot} is allowed to reference Granit.{ModuleRoot}.

When creating a new package:

  1. Does it fall under an existing module root? (e.g., Granit.BlobStorage.Minio) — It is a module sub-package. No action needed.

  2. Does it provide horizontal capability used by 3+ unrelated modules? — It is framework. Ensure it has zero business-domain knowledge.

  3. Is it a new vertical business feature? (e.g., Granit.Scheduling) — It is a new module. Add its root to FrameworkBoundaryRules.ModuleRootPrefixes.

  4. Is it a bridge between framework and module? (e.g., Granit.Encryption.BlobStorage) — Framework sub-package with bridge exemption. The naming convention Granit.{FrameworkFamily}.{ModuleRoot} triggers the exemption automatically.

The FrameworkBoundaryTests architecture test enforces the boundary in CI:

  • Framework_packages_should_not_reference_module_packages — scans every framework .csproj and fails if any ProjectReference points to a module project.
  • Every_src_package_should_be_classified — catches new packages that do not match any category (missing module root registration).

Source: tests/Granit.ArchitectureTests/FrameworkBoundaryTests.cs

  • Module System[DependsOn], topological sort, lifecycle hooks
  • Bundles — meta-packages for quick onboarding
  • Dependency Graph — full package-level dependency diagrams for all 210 packages