Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Generics, Trait Objects, and the Public API

Apalis keeps the job hot path statically typed and erases types mainly where heterogeneous values must coexist.

Public types and traits readers meet

  • Construction and execution: WorkerBuilder, Worker, Monitor, WorkerContext, and Shutdown.
  • Job model: Task, TaskBuilder, ExecutionContext, TaskId, Attempt, Status, Data<T>, Metadata, and Extensions.
  • Backend contract: Backend, TaskSink, and capability traits such as FetchById, Update, Reschedule, Vacuum, and RegisterWorker.
  • Handler contract: TaskFn, FromRequest, and IntoResponse.
  • Composition: Tower’s Service, Layer, Stack, and policy layers for retry, limits, timeout, tracing, and filtering.
  • Failure/lifecycle: BoxDynError, AbortError, RetryAfterError, WorkerError, retry policies, and worker Event.

The prelude makes the common subset feel small; the individual modules retain the precise vocabulary for extension authors.

Where generics are used

Backend uses associated types for Args, Id, Connection, Codec, compact storage form, error, and middleware layer. Task<Args, Connection, Id> carries those choices forward. The handler, extractor tuple, executor, Tower service, layer stack, retry policy, codec, and acknowledgement service are generic too.

This gives direct calls and compile-time compatibility checks in the path that runs for every job.

The builder is a type-level state machine

It starts as:

WorkerBuilder<(), (), (), Identity>

.backend(storage) is available in that initial state and returns a builder whose Args, Connection, and Source come from storage: Backend. Every .layer(...) changes the middleware type to Stack<New, Old>. .build(handler) exists only when the backend and handler form a valid Service<Task<Args, Connection, Backend::Id>>.

Consequences:

  • a missing backend cannot produce a runnable worker;
  • the payload type cannot disagree with the backend;
  • unavailable handler extractors fail compilation;
  • incompatible middleware cannot be assembled;
  • the final service order is encoded in the nested type.

This does not prevent semantic mistakes such as a bad retry count, but it makes structurally invalid configurations hard to represent.

Where trait objects appear

BoxDynError erases many error types at integration boundaries. Event listeners and monitor-owned worker factories/futures are boxed so different concrete workers can share one collection. Some stream/sink and future boundaries are boxed for the same reason.

The rule is practical: use generics where one concrete pipeline is known at build time; erase types where runtime heterogeneity is the feature.