In distributed enterprise architectures, naive background processing introduces severe operational hazards:
- Fragile External Cron Scripts: Relying on server-level Linux cron jobs calling curl endpoints, which fail when nodes scale or drift;
- In-Memory Timer Overruns:
System.Threading.Timertriggering new iterations before previous runs finish, exhausting database pools; - Duplicate Multi-Instance Execution: Deploying 3 API instances causes all 3 to run “Monthly Billing” at midnight, billing customers three times!
BitzOrcas.Modern solves this with a database-backed Quartz cluster + scheduler-agnostic IJobExecutor<T> architecture: Business logic remains pure, while clustered database leases guarantee single-instance execution and automated failover.
Background Job Execution Flow
Step 1: Defining Typed Job Identities and Agnostic Executors
Core rule in BitzOrcas: Business handlers must never reference Quartz IJob or JobExecutionContext directly.
Define a typed identity alongside its IJobExecutor<T>:
using BitzOrcas.Application.Abstractions.Jobs;using BitzOrcas.Domain.Results;
namespace BitzOrcas.Auditing.Application.Jobs;
// 1. Declare immutable job identitypublic static class AuditJobIdentities{ public sealed class RetentionPrune : IBackgroundJobIdentity { public static string JobName => "audit-retention"; // matches the built-in BackgroundJobIdentities entry }}
// 2. Pure business executor: depends only on application ports, zero Quartz dependenciespublic sealed class AuditRetentionJobExecutor( IAuditRetentionPort retentionPort, IAppClock clock) : IJobExecutor<AuditJobIdentities.RetentionPrune>{ public async Task<Result> ExecuteAsync(CancellationToken ct) { // Calculate retention cutoff (prune records older than 180 days) var cutoffTime = clock.UtcNow.AddDays(-180);
// Execute batch pruning via domain port await retentionPort.PruneExpiredLogsAsync(cutoffTime, ct);
return Result.Success(); }}Step 2: Declarative Configuration in JobHost
Configure execution schedules declaratively in appsettings.json:
{ "BackgroundJobs": { "audit-retention": { "Enabled": true, "CronExpression": "0 0 2 * * ?", "IntervalSeconds": 86400 // The override path reads only CronExpression/IntervalSeconds/Enabled; // Description belongs to module declaration and cannot be overridden. // IntervalSeconds and CronExpression are mutually exclusive (fail-loud). } }}- Compile-Time Safety: Unbound job identities cause fast startup failures instead of runtime exceptions;
- Zero-Code Schedule Updates: Modify cron triggers via environment variables without rebuilding binaries.
Step 3: Execution Safety and Graceful Shutdown
Long-running jobs must respect two principles:
- Cursor Batching: Process records in batches of 1,000, committing progress to prevent huge memory footprints;
- Cancellation Respect: Inspect
cancellationToken.IsCancellationRequestedto safely stop at batch boundaries on container termination (SIGTERM).
Summary
BitzOrcas background processing delivers operational predictability:
- Scheduler-Agnostic: Pure business handlers run in unit tests in microseconds;
- Clustered Locking: Database locks eliminate duplicate concurrency;
- Auditing by Default: Full execution history automatically feeds operations dashboards.