Skip to main content
Version: 1.1

15. Architecture Decisions

Four Architecture Decision Records govern this framework, all Status: Accepted, dated 2026-06-30, from the initiative's Phase 0.5 architecture session. Numbers ADR-003 through ADR-006ADR-001/ADR-002 belong to the platform-wide standard and the Notification Framework respectively, not this framework.

15.1 ADR-003 — Background Jobs Tracking Entities Remain ERP/Host-Owned

Context: an early migration plan proposed moving UserJob, ImportJobHistory, and ImportJobRowError into a framework-owned Persistence package, mirroring how the Notification Framework owns its entities outright.

Decision: none of the three has a shape a different ERP could reuse unmodified. UserJob is built around DtParameters/DtResult grid conventions and ERP-specific archive/cancel UX; the import-tracking entities are tenant-scoped, written through TenantDbContext, and carry an ERP JobType discriminator in their schema. These entities — and any future entity of the same shape — stay permanently ERP/host-owned. The framework owns only the interfaces (IBackgroundJobProgressService, IImportJobTrackingService), never their implementation or storage.

Tradeoff accepted: this is a deliberate, permanent structural deviation from the Notification Framework Golden Reference — the Background Jobs Framework has no Persistence package, ever. Accepted because generalizing the entities' shape for a hypothetical second ERP would be speculative design with no real second consumer to validate against. Reopening this requires a new ADR explicitly superseding it.

15.2 ADR-004 — Hangfire Dashboard Remains Host-Owned

Context: the framework exposes a UseHangfireDashboard extension method, but the dashboard's authorization policy, route path, and whether to expose it at all in a given environment are deployment-specific security decisions.

Decision: dashboard authorization, route mounting, and DI wiring stay permanently host-owned; the framework's responsibility stops at exposing the extension method. It never decides on the host's behalf whether, where, or to whom the dashboard is shown. The dashboard's own data (Hangfire's HangFire.* tables) is Hangfire-infrastructure-owned, not a framework-vs-host question at all.

Tradeoff accepted: no framework-level opinionated default (e.g. "Admins only") — rejected because authorization policy depends on each ERP's own role/permission model, and a framework default would either be wrong or force a reverse dependency on host-specific authorization concepts.

15.3 ADR-005 — Framework Owns Execution, ERP Host Owns Visualization

Context: this is a generalizing ADR. Phase 0.5's ownership review kept arriving at the same dividing line for nearly every artifact — scheduling/queueing/dispatch/retry/concurrency is generic and framework-owned; anything presenting a job's state, history, or outcome to a human is ERP-specific and host-owned. ADR-003 and ADR-004 are each one instance of this same underlying rule.

Decision: name the rule explicitly so future artifacts can be classified without re-deriving it: is this part of making the job run, or part of showing the job to a human?

Tradeoff accepted: rejected a generic job-visibility UI/API surface consumed by every ERP, because each ERP's grid columns, archive semantics, and permission model differ enough that a generic surface would either be too abstract to be useful, or re-accumulate ERP-specific options inside the framework anyway.

15.4 ADR-006 — Notifications Must Route Through a Notification Adapter

Context: two mechanisms already produce user-facing notification output from inside the background-jobs subsystem without going through the Notification Framework at all — NotifyUserOnCompletionFilterIUserJobsNotificationBackgroundJobsHub (direct SignalR), and IProductImportMailNotificationServiceIMailService (direct email). The explicit design instruction was: the Background Jobs Framework must not reimplement the Notification Framework.

Decision: all such output must eventually route through a single framework-owned adapter contract (INotificationDispatchAdapter, per the ADR text), implemented by the host and calling into the Notification Framework; the Background Jobs Framework must never itself select a delivery channel. This is explicitly a target-state decision, not an immediate code change — the two existing direct-dispatch mechanisms were left running, flagged only as deprecated-by-target-architecture.

Tradeoff accepted: rejected both "leave the two mechanisms as permanent parallel channels" (would formalize exactly the duplication the design instruction asked to avoid) and "depend directly on Shumoul.Notification.* packages" (would violate the two modules' confirmed-clean, independently-versioned dependency separation).

⚠ Known gap — not yet realized in shipped code. The later implementation phase and the framework's own anti-patterns audit instead name and ship IBackgroundJobNotificationAdapter / BackgroundJobNotificationBoundaryAdapter — a permanent no-op stub. The intended real adapter was never built. In practice, every one of the six migrated jobs bypasses this adapter entirely and dispatches notifications by calling the Notification Framework directly from inside its own worker/service — the ADR-006 target architecture has never actually been exercised in production. This is carried forward as roadmap backlog, not silently abandoned, "until and unless a future ADR explicitly supersedes it." See §20 — Appendix (tracked as AP-03).

15.5 Decisions resolved directly in phase documents (never elevated to a formal ADR)

For completeness — these are real, binding architectural decisions, just not formal ADRs:

  • Tenant context resolution via IMultiTenantContextAccessor rather than claims-based lookup (Phase 1.5).
  • Distributed locking via a real Hangfire storage-connection call rather than a broken StateData-keyed check (Phase 1.5).
  • The pipeline is the sole orchestrator — no separate "coordinator" service (Phase 2A.1).
  • Per-row claiming over a distributed lock for duplicate-execution prevention (Phase 3.8/3.10) — see §9 — Distributed Lock.
  • The two-tier adapter model — framework-owned infrastructure adapters vs. host-owned business-job adapters (Phase 2B.3, formalized at Phase 4 certification).
  • Migrate the campaign scheduler before the campaign executor (Phase 3.8).
  • Treat Golden Reference certification and LTS as separate, deliberately fix-nothing documentation phases (Phase 4, Phase 4.1) — no code changes during certification itself.

15.6 What "closed" permits — ordinary maintenance vs. a new initiative

Allowed as ordinary maintenance, no new initiative needed:

  1. Bug fixes that don't change any Stable public signature (PATCH version).
  2. Security fixes and dependency bumps (even if MINOR/MAJOR).
  3. Performance improvements with no observable Stable-API behavior change.
  4. Any change explicitly authorized by a new, accepted ADR.
  5. Closing one of the four pre-identified, pre-approved documented gaps (AP-01AP-04).
  6. New job migrations onto the existing pipeline — explicitly excluding the three deferred jobs (Product Import, Tenant Provisioning, DatabaseInitializer), which require a new approved initiative regardless of mechanical similarity to the six already completed.

Not allowed without a new initiative or ADR:

  1. Architecture redesign — the 5-package shape, the dependency law, the two-tier adapter model.
  2. Runtime redesign — changing what the pipeline, executor, scheduler, or classifier fundamentally do (as distinct from closing a documented gap, which is allowed).
  3. Public API breaking changes to anything Stable, outside the documented MAJOR-version process.
  4. Package restructuring — merging, splitting, renaming, or adding a package, including reopening the "should this framework have its own Persistence package" question ADR-003 already closed permanently.
  5. Reassigning any row of the frozen Ownership Matrix.
  6. Migrating any of the three deferred jobs.
  7. Reopening a permanently rejected idea (e.g. a job-level distributed lock, or moving UserJob into the framework) without a new ADR.

Enforcement is procedural, not automated — there is no tooling gate today; every reviewer, human or AI assistant, is expected to check a proposed change against this policy before proceeding.