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, andShutdown. - Job model:
Task,TaskBuilder,ExecutionContext,TaskId,Attempt,Status,Data<T>,Metadata, andExtensions. - Backend contract:
Backend,TaskSink, and capability traits such asFetchById,Update,Reschedule,Vacuum, andRegisterWorker. - Handler contract:
TaskFn,FromRequest, andIntoResponse. - 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 workerEvent.
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.