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
| Owner | Owns |
|---|---|
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.