Backend ADRs — Architecture Decision Records
Architecture Decision Records (ADRs) document significant technical decisions made during the development of Granit.
Each ADR follows a consistent template: Context, Decision, Evaluated Alternatives, Justification, and Consequences.
ADR index
Section titled “ADR index”| # | Title | Status | Date | Scope |
|---|---|---|---|---|
| 001 | Observability Stack — Serilog + OpenTelemetry | Accepted | 2026-02-21 | Granit.Observability |
| 002 | Redis via StackExchange.Redis — Distributed Cache | Accepted | 2026-02-21 | Granit.Caching |
| 003 | Testing Stack — xUnit v3, NSubstitute and Bogus | Accepted | 2026-02-21 | granit-dotnet |
| 004 | Asp.Versioning — REST API Versioning | Accepted | 2026-02-22 | Granit.Http.ApiVersioning |
| 005 | Wolverine + Cronos — Messaging, CQRS and Scheduling | Accepted | 2026-02-22 | Granit.Wolverine |
| 006 | FluentValidation — Business Validation Framework | Accepted | 2026-02-24 | Granit.Validation |
| 007 | Testcontainers — Containerized Integration Tests | Accepted | 2026-02-24 | Integration Tests |
| 008 | SmartFormat.NET — CLDR Pluralization | Accepted | 2026-02-26 | Granit.Localization |
| 009 | Scalar.AspNetCore — Interactive API Documentation | Accepted | 2026-02-26 | Granit.Http.ApiDocumentation |
| 010 | Scriban — Text Template Engine | Accepted | 2026-02-27 | Granit.Templating.Scriban |
| 011 | ClosedXML — Excel Spreadsheet Generation | Accepted | 2026-02-27 | Granit.DocumentGeneration.Excel |
| 012 | PuppeteerSharp — HTML to PDF Rendering | Accepted | 2026-02-28 | Granit.DocumentGeneration.Pdf |
| 013 | Magick.NET — Image Processing | Accepted | 2026-02-28 | Granit.Imaging.MagickNet |
| 014 | Migrate FluentAssertions to Shouldly | Accepted | 2026-02-28 | granit-dotnet |
| 015 | Sep — High-Performance CSV Parsing | Accepted | 2026-03-01 | Granit.DataExchange.Csv |
| 016 | Sylvan.Data.Excel — Streaming Excel File Reading | Accepted | 2026-03-01 | Granit.DataExchange.Excel |
| 017 | DDD Aggregate Root & Value Object Strategy | Accepted | 2026-03-19 | granit-dotnet (all modules) |
| 018 | FusionCache — Caching Provider | Accepted | — | Granit.Caching |
| 019 | User Lookup — Dual Mode | Accepted | — | Granit.Identity |
| 020 | Declarative Definitions Placement (Query & Export) | Accepted | 2026-04-18 | Granit.QueryEngine, Granit.DataExchange, all modules |
| 021 | Privacy Data Export — Framework Defaults | Accepted | 2026-04-19 | Granit.Privacy, Granit.Privacy.BlobStorage, Granit.Privacy.EntityFrameworkCore |
| 022 | ICommandSender + Module Naming — No Technology Suffix on Domain Modules | Accepted | 2026-04-20 | All business modules, Granit.Commands, Granit.Wolverine |
| 023 | Tenant-aware role lookup | Accepted | 2026-04-22 | Granit.Identity.Local, Granit.Identity.Local.AspNetIdentity |
| 024 | Shared-connection EF Core transaction for role orchestration | Accepted | 2026-04-22 | Granit.Identity.Local.AspNetIdentity, Granit.Authorization.EntityFrameworkCore, Granit.OpenIddict.EntityFrameworkCore, Granit.Persistence.EntityFrameworkCore |
| 025 | Keycloak client-role distinction and boot-time sync | Accepted | 2026-04-22 | Granit.Identity, Granit.Identity.Federated.Keycloak |
| 026 | Entra ID App Role distinction and boot-time sync | Accepted | 2026-04-22 | Granit.Identity.Federated.EntraId |
| 027 | Cognito app-client group sync via naming-prefix convention | Accepted | 2026-04-23 | Granit.Identity.Federated.Cognito |
| 028 | Unified Data Lookup for QueryEngine Filters and Form Dropdowns | Accepted | 2026-04-23 | Granit.DataLookup.*, Granit.QueryEngine.*, Granit.ReferenceData.*, @granit/data-lookup, @granit/react-data-lookup |
| 029 | Client-role sync — opt-in orphan cleanup policy | Accepted | 2026-04-23 | Granit.Authorization.*, Granit.Identity.Federated.* |
| 030 | Client-role sync — scheduled re-sync via Granit.BackgroundJobs | Accepted | 2026-04-23 | Granit.Identity.Federated.*.BackgroundJobs |
| 031 | Client-role write operations on IIdentityClientRoleManager | Accepted | 2026-04-23 | Granit.Identity, Granit.Identity.Federated.* |
| 032 | Granit.Catalog with Product as the shared billing aggregate | Accepted | 2026-04-24 | Granit.Catalog.*, Granit.Metering, Granit.Subscriptions, Granit.Invoicing |
| 033 | Metering hybrid — lifecycle, CountDistinct, recompute, backfill, deprecate | Accepted | 2026-04-25 | Granit.Metering.* |
| 034 | Subscriptions — pricing tiers, phases, discounts, price overrides | Accepted | 2026-04-25 | Granit.Subscriptions, Granit.Subscriptions.EntityFrameworkCore, Granit.Subscriptions.Endpoints |
| 035 | Granit.CustomerBalance ↔ ORB Credits term-by-term mapping | Accepted | 2026-04-25 | Granit.CustomerBalance.* |
| 036 | Invoicing line item source + product convention | Accepted | 2026-04-25 | Granit.Invoicing.*, Granit.Subscriptions, Granit.Metering |
| 037 | Party Merge Framework — Mergeable primitive + cross-module rewriters | Accepted | 2026-04-27 | Granit.EntityMerge, Granit.EntityMerge.EntityFrameworkCore, Granit.Parties, Granit.Parties.Mergeable, Granit.Parties.Endpoints |
| 038 | DashboardDefinition vs Dashboard boundary | Accepted | 2026-04-28 | Granit.Analytics, Granit.Analytics.EntityFrameworkCore, Granit.Analytics.Endpoints |
| 039 | Widget renderer architecture — typed IWidgetSource<TSnapshot> + non-generic adapter | Accepted | 2026-04-29 | Granit.Dashboards, Granit.Dashboards.Endpoints, Granit.Analytics |
| 040 | Three-tier metadata architecture (compiled / tenant customization / Niveau B) | Accepted | 2026-05-01 | Granit.Entities, Granit.Entities.Customization |
| 041 | Field component catalog + closed naming convention | Accepted | 2026-05-01 | Granit.Entities (form/detail builders); @granit/entities-react |
| 042 | View catalog + per-kind config schema | Accepted | 2026-05-01 | Granit.Entities (collections facet); @granit/entities-react |
| 043 | Dashboard push transport (SSE / WebSocket) is framework-owned | Accepted | 2026-04-30 | Granit.Analytics, Granit.Dashboards, Granit.Dashboards.Push |
| 044 | Workspace navigation — hierarchical + additive presets + agnostic detail routes | Accepted | 2026-04-30 | Granit.Workspaces.*, @granit/workspaces-react |
| 045 | Inversion-of-control contributor pattern (workspaces, relations, activities) | Accepted | 2026-04-30 | Granit.Workspaces.Abstractions, Granit.Entities.Abstractions, Granit.Activities.Abstractions |
| 046 | Activities vs Timeline split — past vs future, narrative vs action-required | Accepted | 2026-04-30 | Granit.Timeline, Granit.Activities |
| 047 | EntityView supersedes Granit.QueryEngine.SavedViews | Accepted | 2026-04-30 | Granit.Entities.Views, Granit.Entities.Views.Endpoints; @granit/entities-react |
| 048 | Cross-module entity relations (smart buttons + sidebar + tab + inline-chips) | Accepted | 2026-05-01 | Granit.Entities.Abstractions, Granit.Entities, Granit.Entities.Endpoints |
| 049 | Default landing route — 5-tier precedence with URL/route output | Accepted | 2026-04-30 | Granit.Workspaces, Granit.Workspaces.Endpoints |
| 050 | OData EDM whitelist via EntityDefinition (gate) + ExportDefinition (field source) | Accepted | 2026-05-01 | Granit.Http.ODataExposure |
| 051 | User aggregate in Granit.Identity + optional Granit.Parties.Identity bridge | Accepted | 2026-05-02 | Granit.Identity, Granit.Identity.Local, Granit.Identity.Federated, Granit.Authorization, Granit.Parties.Identity |
| 052 | Granit.Documents — user-managed file & asset module | Accepted | 2026-05-10 | Granit.Documents.* (+ Renditions, AssetMetadata, PublicLinks, Indexing, Workflow, Collections) |
| 053 | Granit.Entities.Customization — Layer 1 tenant overrides (reorder / regroup / hide) | Accepted | 2026-05-02 | Granit.Entities.Customization.* (+ touches Granit.Entities.Endpoints) |
| 054 | Granit.Taxonomy — cross-cutting tags and hierarchical categories | Accepted | 2026-05-02 | Granit.Taxonomy.* (replaces per-module *Tag join tables) |
| 055 | Extract URL safety + temp-file primitives into shared packages | Accepted | 2026-05-11 | Granit.Http.Security, Granit.IO; consumers refactored: Browsing, Webhooks, Privacy.BlobStorage |
| 056 | Server-side entity-action execution + bulk endpoint | Accepted | 2026-05-13 | Granit.Entities.Abstractions (Actions/Execution/), Granit.Entities.Endpoints (BulkActionEndpoint) |
| 057 | Workspace composition belongs to the application, not the module | Accepted | 2026-05-15 | Granit.Workspaces.*, every *.Endpoints package shipping a *WorkspaceContribution |
| 058 | JSON Persistence Policy | Accepted | 2026-05-21 | Granit.Persistence |
| 059 | Enum persistence strategy | Accepted | 2026-05-24 | Granit.Persistence |
| 060 | OIDC sub-as-Guid policy for domain ownership | Accepted | 2026-05-26 | Granit.Users, Granit.Documents |
| 061 | Optimistic concurrency strategy | Accepted | 2026-05-27 | Granit.Persistence |
| 062 | Framework-pure core + transport bindings (.Http / .Wolverine) | Accepted | 2026-05-27 | Granit.RateLimiting.*, Granit.Features.* |
| 063 | Tenant/Host data storage modes — reverted to single-DbContext baseline | Superseded | 2026-05-31 | Granit.Persistence, all *.EntityFrameworkCore packages |
| 064 | Structured AI output as a first-class Granit.AI primitive | Accepted | 2026-05-30 | Granit.AI, Granit.AI.Extraction, all .AI consumer modules |
| 065 | Privacy capability modularization — extract Data Export as an opt-in module | Accepted | 2026-06-03 | Granit.Privacy, Granit.Privacy.DataExport, Granit.Parties.Privacy, Granit.Documents.Privacy |
| 066 | Account creation — master gate, default-role provisioning, external profile completion | Accepted | 2026-06-11 | Granit.Identity.Local, Granit.Identity.Local.Endpoints, Granit.OpenIddict |
| 067 | Agentic AI chat, tool registry, and prompt catalog | Accepted | 2026-06-12 | Granit.AI.Tools, Granit.AI.Chat, Granit.AI.Prompts (+ QueryEngine.AI / VectorData / Localization.AI adapters) |
| 068 | Validation message key naming convention — Validation:{Category}:{Rule} (framework) / {Module}:Validation:{Rule} (modules), enforced by an architecture test | Accepted | 2026-06-11 | Granit.Validation + every module owning validation messages (framework, granit-business, granit-iot) |
| 069 | Bank accounts as the single source of truth — centralized referential, masked snapshots, immutable signed mandate snapshot | Accepted | 2026-06-14 | Granit.BankAccounts.*, Granit.Payments.SepaDirectDebit.*, Granit.Payments.SepaTransfer.* |
| 070 | Value-object persistence & query capabilities — converter (default) vs ComplexProperty opt-in vs read-model projection; fail-loud QueryEngine contract | Accepted | 2026-06-15 | Granit.Domain.SingleValueObject, ApplyGranitConventions, Granit.QueryEngine.* |
| 071 | Agentic chat streaming via FunctionInvokingChatClient — early-flush SSE + live tool-status frames | Accepted | 2026-06-18 | Granit.AI.Tools, Granit.AI.Chat, Granit.AI.Chat.Endpoints (+ Microsoft.Extensions.AI) |
| 072 | Address platform — three orthogonal axes, flat-column persistence, capability model, tiered verification | Accepted | 2026-06-29 | Granit.Domain.ValueObjects (Address*), Granit.Geocoding.*, Granit.AddressEnrichment, Granit.AddressDeliverability.Abstractions; consumed by granit-business Parties |