# Execution Model

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. Because it persists nothing, the In-Memory Store cannot carry the
execution guarantee, so its home is tests and local development.

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](/docs/advanced/determinism-boundary). For the Core
as a state machine that steps events into decisions, see
[The Sans-IO Node Driver](/docs/advanced/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](/docs/core-concepts/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](/docs/storage/overview), and for the seam
itself see [the Storage Contract reference](/docs/reference/storage-contract).

## 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](/docs/storage/wakeup-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](/docs/core-concepts/execution-guarantee), and the full
scope lifetime rules live in
[The Job Pump and DI Scope](/docs/advanced/job-pump).

## 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](/docs/advanced/throughput-model).

## Where to go next

- [The Determinism Boundary](/docs/advanced/determinism-boundary): exactly what is
  simulable, the seeded simulation, fault injection, and Virtual Time.
- [The Sans-IO Node Driver](/docs/advanced/node-driver): the Core as a state
  machine that steps events into decisions, the claim loop, and Lease renewal.
- [The Job Pump and DI Scope](/docs/advanced/job-pump): the full per-Attempt scope
  mechanics and lifetime rules.
- [Throughput and Pump Fan-Out](/docs/advanced/throughput-model): Pumps versus
  Workers, fan-out for throughput, and the poll-interval tradeoffs.
- [Execution Guarantee](/docs/core-concepts/execution-guarantee): at-least-once
  execution, the Effect-Once fence, and the idempotency patterns.
- [Job Lifecycle](/docs/core-concepts/job-lifecycle): the state machine the Core
  drives and how a Lease expiry counts as an Attempt.
- [Storage Overview](/docs/storage/overview): the adapters and topologies behind
  the storage boundary.
- [Storage Contract reference](/docs/reference/storage-contract): the storage
  boundary as a reference seam.
