Execution Model

BackWave needs no leader, no coordinator, and no Redis. The database row is the only truth, and every node is a stateless peer that claims work by polling.


BackWave splits its runtime into two halves. A pure Core makes every decision (when a job is due, whether a failure retries or dead-letters, when a Lease has expired, which Queue to claim from) as plain functions of state, event, and time. A thin Shell does everything else: it polls the database, hands the current state to the Core, and carries out the resulting instructions. The database row is the single authoritative truth for every job, and nodes are interchangeable stateless peers. You scale by adding processes. There is no leader, no coordinator, no message broker, and no Redis. Your job logic stays deterministic and testable on a virtual clock.

This page is the conceptual map. It rests on four pillars, and each one links down to an Advanced page that owns the deep mechanics.

The Core decides, the Shell acts#

The Core is pure decision logic. Given a snapshot of state, an event, and the current time, it returns the decisions that follow. It performs no I/O and never reads the wall clock. The Shell is the imperative loop around it. It fetches state, calls the Core, and executes what the Core decided through the storage boundary. It owns all the concurrency, the sockets, and the real clock. The Shell is kept deliberately simple, so the interesting logic lives in the part that touches nothing.

That split is what makes your job flows testable. Because the Core does no I/O and never reads the wall clock, the same inputs always produce the same decisions. BackWave ships a public In-Memory Store that runs on Virtual Time, so you can drive scheduling and retry scenarios deterministically, advancing the clock through long stretches of activity in milliseconds with no database in the loop. The In-Memory Store is for tests and local development, never a production target.

The practical payoff is that the only moving part you operate is the database you already run. The decision logic is a library, not a service, so there is no extra process to deploy, watch, or fail over.

For the full picture of what is simulable and how the seeded simulation works, see The Determinism Boundary. For the Core as a state machine that steps events into decisions, see The Sans-IO Node Driver.

The database is the only truth, and nodes are stateless peers#

Every job's state lives in its database row, and that row is the only truth. Nodes hold only ephemeral working state, a heartbeated Lease here, a Dispatch Policy counter there, nothing another node would need to recover. So every node is an identical peer. Add more app instances and they share the same database, claiming work cooperatively, and the database itself arbitrates who wins a contested claim. That is why there is no broker to run and no lock service to operate.

Horizontal scaling falls out of this directly. More replicas against the same database means more claiming capacity, and the model fits the way ASP.NET apps already deploy: scale out, recycle, no pets. It also means a network fault is not a crisis. An isolated node cannot fork the authoritative state. It can only act on a stale read of it, so there is no split-brain to reconcile. The fence that makes a late write from a recovered node harmless belongs to Execution Guarantee.

The storage boundary where nodes meet the database is implemented by a Storage Adapter. Networked Adapters back Postgres and SQL Server, where the cluster spans many machines, and an Embedded Adapter backs SQLite, where many processes on one host still coordinate through the shared file. For the adapters and topologies, see Storage Overview, and for the seam itself see the Storage Contract reference.

Polling is the truth, and hints only lower latency#

Workers acquire jobs through one mechanism: a bounded, batched polling claim loop against the database. That polling is the sole source of correctness. Where a database offers a notification channel, BackWave uses it as a Wake-Up Hint, a nudge that says "something was enqueued, poll now" so a Worker starts an earlier poll instead of waiting for the next scheduled one. A hint only lowers latency. Drop it, duplicate it, or delay it arbitrarily, and nothing changes but how soon a job starts.

This is why behavior is the same on every supported database. Postgres has a notification channel, so an enqueue can turn into a start in milliseconds where a hint is available, a sharper response than a fixed polling interval gives you on its own. SQL Server is polling-only and is fully correct without any hint at all. You never write code that depends on a hint arriving.

The hint mechanics and how latency behaves are covered in Wake-Up Hints.

A fresh scope per Attempt#

Each Attempt runs inside its own DI scope. The handler is resolved from that scope, so any scoped dependency it injects is created fresh for that try and disposed when the try ends. The scope wraps exactly one Attempt's invocation, which lines up with the domain meaning of an Attempt: one execution try, where a Lease expiry counts the same as a thrown exception.

That fresh scope is what makes the idempotency patterns ergonomic. A handler can inject a scoped DbContext and use it for the dedupe or conditional write that keeps the work safe to run more than once, and disposal is correct because the scope closes at the end of each try.

The rule of thumb for handler authors is short. Default your handlers to scoped, which is what the generated registrations already do, and default the class that declares [Job] methods to scoped as well, since it behaves like a controller for jobs, resolved once per Attempt. Reserve singleton for genuinely process-wide state, and inject that singleton into your handler rather than making the handler itself a singleton. A singleton handler is shared across every Attempt and cannot depend on scoped services, which is the one hazardous choice here.

The idempotency patterns themselves live in Execution Guarantee, and the full scope lifetime rules live in The Job Pump and DI Scope.

Where the storage boundary sits#

The four pillars line up cleanly around one seam. The Core sits inside the storage boundary as the pure brain, simulable and deterministic. The Shell straddles the boundary, running your handlers, opening the per-Attempt scope, holding the real clock and the sockets, and reaching across the seam to the database. The database sits beyond the boundary as the single source of truth.

A few low-level knobs, like the poll interval and the delivery timeout, are Shell concerns. They shape latency and throughput, never correctness or determinism, so tuning them changes how fast work moves, not which decisions get made. The numbers behind that, and how adding nodes scales throughput, live in Throughput and Pump Fan-Out.

Where to go next#