Skip to main content
Version: 1.2

2. Architecture

2.1 Layer diagram

2.2 Dependency graph and package graph

Full package-by-package breakdown, allowed dependency directions, and publication/distribution mechanics live in Chapter 3 — Packages. The one-sentence summary: Contracts is a zero-dependency leaf; Abstractions depends only on Contracts; Core and Adapters are siblings (neither references the other, both depend only on Abstractions); the entry point depends only on Core; the ERP host references only the entry point and Adapters, always via PackageReference, never ProjectReference.

2.3 Ownership

OwnerOwns
Shumoul.BackgroundJobs.* (the framework)Pipeline orchestration, execution wrapping, scheduling abstraction, tenant-context scoping, the distributed-lock primitive, failure classification, its own infrastructure adapters
ERP Host (Shumoul.Saas.Api)Every job-specific adapter, every Hangfire-callable bridge, recurring-job registration and cutover, DI composition order, Hangfire Dashboard authorization/exposure (permanently, ADR-004), job-tracking/visibility persistence (permanently, ADR-003), and all business logic for any job it hosts

See §1.8 and §15 — Architecture Decisions for the full reasoning and the ADRs that froze this table.

2.4 Layer responsibilities, one line each

  • Contracts — what a job execution is, as data. No behavior.
  • Abstractions — what a job execution does, as contracts. No implementation.
  • Core — the actual runtime engine implementing those contracts, with zero knowledge of Hangfire, EF Core, or any ERP concept.
  • Adapters (framework) — the one place Hangfire and MultiTenancy types are allowed to appear inside the framework's own packages.
  • Entry point — one method call's worth of DI wiring, nothing else.
  • ERP host — everything genuinely specific to Shumoul Cloud ERP: which jobs exist, what they do, how they're scheduled, how their outcomes are surfaced to a human.

2.5 Execution model, thread model, lifetime model

Execution model: every job execution is a single async Task-based invocation running on whichever Hangfire worker thread claimed the job — Hangfire's own bounded worker thread pool. The framework introduces no threads, no custom scheduler loop, and no execution model of its own; it rides entirely on Task/async/ await over whatever thread Hangfire's server assigns.

Thread model: there is no thread affinity requirement anywhere in the framework's public API — every interface method is async, and nothing depends on SynchronizationContext or a specific thread identity.

Lifetime model (DI): see the full table in §6.8. In one sentence: every runtime service is Transient except IBackgroundJobTenantContext, which is deliberately Scoped so tenant identity stays consistent for the duration of one job's DI scope; the executor and job-specific adapters are never DI-registered at all — they are constructed with new, once per execution, by the bridge.

Retry lifetime: a Hangfire-driven retry re-invokes the entire bridge method from scratch — a fresh Hangfire job-activation DI scope, a fresh BackgroundJobExecutionContext, fresh adapter and executor instances, and a fresh pipeline call. No retry-count-aware state persists inside the framework between attempts; retry counting and backoff are entirely Hangfire's own, unmodified default behavior.

2.6 What this chapter deliberately does not repeat

The full ten-concept runtime sequence (Scheduler → Pipeline → Execution Context → Tenant Context → Distributed Lock → Executor → Adapter → Business Service → Result → Completion), including the important distinction between what the pipeline does automatically versus what a job author composes manually, is covered in full in Chapter 4 — Runtime Pipeline and Chapter 5 — Execution Flow — read those two chapters together with this one for the complete runtime picture.